visual-remote 0.1.0 → 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.
- package/README.md +35 -23
- package/apps/cli/dist/index.js +153 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -16,13 +16,23 @@
|
|
|
16
16
|
Node.js와 pnpm 버전은 각각 `.nvmrc`와 `package.json`에 고정되어 있습니다.
|
|
17
17
|
다른 Node.js 버전에서는 `engine-strict` 설정으로 설치가 중단됩니다.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## 가장 빠른 사용
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
기존 앱이 실행 중인 프로젝트에서 그 주소만 전달합니다.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx --yes visual-remote@latest http://localhost:9011
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
명령이 출력한 `Gateway` 주소로 접속하면 Visual Remote가 포함된 같은 화면이
|
|
28
|
+
열립니다. 원래 `localhost:9011` 화면은 그대로 유지됩니다. `10001`이 사용 중이면
|
|
29
|
+
`10002`, `10003` 순서로 빈 Gateway 포트를 자동 선택합니다.
|
|
30
|
+
|
|
31
|
+
전역 설치를 선호하면 다음처럼 사용합니다.
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
npm install --global visual-remote
|
|
25
|
-
visual
|
|
35
|
+
visual http://localhost:9011
|
|
26
36
|
```
|
|
27
37
|
|
|
28
38
|
## 빠른 시작
|
|
@@ -72,26 +82,23 @@ corepack pnpm test
|
|
|
72
82
|
|
|
73
83
|
## 기존 개발 서버에 연결
|
|
74
84
|
|
|
75
|
-
먼저 대상
|
|
76
|
-
|
|
85
|
+
먼저 대상 애플리케이션을 평소처럼 실행합니다. 다음 예시는 애플리케이션이
|
|
86
|
+
`localhost:9011`에서 실행 중인 경우입니다.
|
|
77
87
|
|
|
78
88
|
```bash
|
|
79
|
-
|
|
80
|
-
--upstream http://127.0.0.1:10002 \
|
|
81
|
-
--listen 10001 \
|
|
82
|
-
--public-url https://visual.example.com
|
|
89
|
+
npx --yes visual-remote@latest http://localhost:9011
|
|
83
90
|
```
|
|
84
91
|
|
|
85
|
-
브리지는 `
|
|
92
|
+
브리지는 `10001`부터 빈 포트를 찾아 다음과 같은 주소를 출력합니다.
|
|
86
93
|
|
|
87
94
|
```text
|
|
88
|
-
Gateway: http://
|
|
89
|
-
|
|
90
|
-
Open:
|
|
95
|
+
Gateway: http://localhost:10001
|
|
96
|
+
Upstream: http://localhost:9011/
|
|
97
|
+
Open: http://localhost:10001
|
|
91
98
|
```
|
|
92
99
|
|
|
93
|
-
브라우저에서는 출력된
|
|
94
|
-
필요하지 않습니다.
|
|
100
|
+
브라우저에서는 출력된 `Gateway` 또는 `Open` 주소를 엽니다. 별도의 페어링 링크나
|
|
101
|
+
토큰은 필요하지 않습니다.
|
|
95
102
|
|
|
96
103
|
Portr를 사용한다면 원래 개발 서버 포트가 아니라 브리지 Gateway 포트 `10001`을
|
|
97
104
|
노출해야 합니다. 앱 화면, 개발 서버의 HMR, 브리지 제어 채널이 한 출처를
|
|
@@ -99,10 +106,11 @@ Portr를 사용한다면 원래 개발 서버 포트가 아니라 브리지 Gate
|
|
|
99
106
|
항상 사용한다면 `.visualdev/config.local.yaml`의 `gateway.publicUrl`로도 설정할 수
|
|
100
107
|
있습니다.
|
|
101
108
|
|
|
102
|
-
##
|
|
109
|
+
## 선택 사항: 앱 실행까지 함께 관리
|
|
103
110
|
|
|
104
|
-
저장소
|
|
105
|
-
|
|
111
|
+
대상 저장소 루트에서 `visual init`을 실행하면 패키지 매니저와 `dev` 스크립트를
|
|
112
|
+
감지해 `.visualdev/config.yaml`을 생성합니다. 기존 파일은 덮어쓰지 않습니다.
|
|
113
|
+
직접 작성할 때 명령은 셸 문자열이 아니라 인자 배열로 작성합니다.
|
|
106
114
|
|
|
107
115
|
```yaml
|
|
108
116
|
version: 1
|
|
@@ -113,7 +121,7 @@ project:
|
|
|
113
121
|
|
|
114
122
|
gateway:
|
|
115
123
|
host: 0.0.0.0
|
|
116
|
-
port:
|
|
124
|
+
port: auto
|
|
117
125
|
# publicUrl: https://visual.example.com
|
|
118
126
|
|
|
119
127
|
upstream:
|
|
@@ -200,14 +208,18 @@ Overlay에서 `작업 보드 ↗`를 다시 눌러 새 세션을 엽니다. 연
|
|
|
200
208
|
현재 제공하는 명령은 다음과 같습니다.
|
|
201
209
|
|
|
202
210
|
```bash
|
|
211
|
+
npx visual-remote http://localhost:9011
|
|
212
|
+
visual init
|
|
203
213
|
visual attach --help
|
|
204
214
|
visual dev --help
|
|
205
215
|
visual status
|
|
206
216
|
visual doctor
|
|
207
217
|
```
|
|
208
218
|
|
|
209
|
-
-
|
|
210
|
-
- `
|
|
219
|
+
- 기본 명령: 전달한 기존 앱 주소 앞에 브리지를 연결합니다.
|
|
220
|
+
- `attach`: 기본 명령의 명시적 이름이며 기존 사용법과 호환됩니다.
|
|
221
|
+
- `init`: 선택적으로 앱 실행까지 관리할 때 기본 설정을 생성합니다.
|
|
222
|
+
- `dev`: `init` 설정을 사용해 앱과 브리지를 함께 실행하는 선택 명령입니다.
|
|
211
223
|
- `status`: 현재 Git 작업 트리의 브리지 실행 상태를 확인합니다.
|
|
212
224
|
- `doctor`: Git, 설정 파일, 개발 명령과 Codex 사용 가능 여부를 점검합니다.
|
|
213
225
|
|
|
@@ -273,5 +285,5 @@ node --version
|
|
|
273
285
|
### 설정 파일 경고가 표시되는 경우
|
|
274
286
|
|
|
275
287
|
기존 서버에 연결하는 `attach`는 설정 파일 없이도 기본값으로 실행할 수 있습니다.
|
|
276
|
-
브리지가 개발 서버를 직접 관리해야 한다면
|
|
277
|
-
|
|
288
|
+
브리지가 개발 서버를 직접 관리해야 한다면 `visual init`을 실행한 뒤 `doctor`를
|
|
289
|
+
다시 실행합니다.
|
package/apps/cli/dist/index.js
CHANGED
|
@@ -565,7 +565,7 @@ Content-Length: 0\r
|
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
function gatewayDisplayHost(host) {
|
|
568
|
-
if (host === "0.0.0.0" || host === "::") return "
|
|
568
|
+
if (host === "0.0.0.0" || host === "::") return "localhost";
|
|
569
569
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
570
570
|
}
|
|
571
571
|
function createGatewayServer(options) {
|
|
@@ -5174,6 +5174,134 @@ function formatDoctorChecks(checks) {
|
|
|
5174
5174
|
}).join("\n");
|
|
5175
5175
|
}
|
|
5176
5176
|
|
|
5177
|
+
// src/init.ts
|
|
5178
|
+
import { readFile as readFile5, mkdir as mkdir3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
5179
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
|
|
5180
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
5181
|
+
function isMissingFile(error) {
|
|
5182
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
5183
|
+
}
|
|
5184
|
+
async function fileExists2(path) {
|
|
5185
|
+
try {
|
|
5186
|
+
await stat3(path);
|
|
5187
|
+
return true;
|
|
5188
|
+
} catch (error) {
|
|
5189
|
+
if (isMissingFile(error)) return false;
|
|
5190
|
+
throw error;
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
function packageManagerFromField(value) {
|
|
5194
|
+
if (typeof value !== "string") return void 0;
|
|
5195
|
+
const name = value.split("@", 1)[0];
|
|
5196
|
+
return name === "bun" || name === "npm" || name === "pnpm" || name === "yarn" ? name : void 0;
|
|
5197
|
+
}
|
|
5198
|
+
async function detectPackageManager(repoRoot, manifest) {
|
|
5199
|
+
const declared = packageManagerFromField(manifest.packageManager);
|
|
5200
|
+
if (declared !== void 0) return declared;
|
|
5201
|
+
const lockfiles = [
|
|
5202
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
5203
|
+
["yarn", "yarn.lock"],
|
|
5204
|
+
["bun", "bun.lock"],
|
|
5205
|
+
["bun", "bun.lockb"],
|
|
5206
|
+
["npm", "package-lock.json"]
|
|
5207
|
+
];
|
|
5208
|
+
for (const [manager, filename] of lockfiles) {
|
|
5209
|
+
if (await fileExists2(join5(repoRoot, filename))) return manager;
|
|
5210
|
+
}
|
|
5211
|
+
return "npm";
|
|
5212
|
+
}
|
|
5213
|
+
function readDevScript(manifest) {
|
|
5214
|
+
if (typeof manifest.scripts !== "object" || manifest.scripts === null || !("dev" in manifest.scripts) || typeof manifest.scripts.dev !== "string" || manifest.scripts.dev.trim().length === 0) {
|
|
5215
|
+
throw new Error('visual init requires a non-empty "dev" script in package.json');
|
|
5216
|
+
}
|
|
5217
|
+
return manifest.scripts.dev.trim();
|
|
5218
|
+
}
|
|
5219
|
+
function devCommand(manager, script) {
|
|
5220
|
+
const command = manager === "pnpm" ? ["corepack", "pnpm", "run", "dev"] : manager === "yarn" ? ["corepack", "yarn", "run", "dev"] : manager === "bun" ? ["bun", "run", "dev"] : ["npm", "run", "dev"];
|
|
5221
|
+
const next = /(^|[\s;&|])next(?:\s|$)/.test(script);
|
|
5222
|
+
return [
|
|
5223
|
+
...command,
|
|
5224
|
+
"--",
|
|
5225
|
+
next ? "--hostname" : "--host",
|
|
5226
|
+
"0.0.0.0",
|
|
5227
|
+
"--port",
|
|
5228
|
+
"{upstreamPort}"
|
|
5229
|
+
];
|
|
5230
|
+
}
|
|
5231
|
+
async function readManifest(repoRoot) {
|
|
5232
|
+
const manifestPath = join5(repoRoot, "package.json");
|
|
5233
|
+
let source;
|
|
5234
|
+
try {
|
|
5235
|
+
source = await readFile5(manifestPath, "utf8");
|
|
5236
|
+
} catch (error) {
|
|
5237
|
+
if (isMissingFile(error)) {
|
|
5238
|
+
throw new Error("visual init requires package.json at the Git worktree root");
|
|
5239
|
+
}
|
|
5240
|
+
throw error;
|
|
5241
|
+
}
|
|
5242
|
+
let value;
|
|
5243
|
+
try {
|
|
5244
|
+
value = JSON.parse(source);
|
|
5245
|
+
} catch (error) {
|
|
5246
|
+
throw new Error(`Unable to parse ${manifestPath}`, { cause: error });
|
|
5247
|
+
}
|
|
5248
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5249
|
+
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
5250
|
+
}
|
|
5251
|
+
return value;
|
|
5252
|
+
}
|
|
5253
|
+
async function initializeVisualDev(dependencies = {}) {
|
|
5254
|
+
const repoRoot = await discoverGitWorktreeRoot(dependencies.cwd ?? process.cwd());
|
|
5255
|
+
const configPath = join5(repoRoot, CONFIG_PATH);
|
|
5256
|
+
if (await fileExists2(configPath)) {
|
|
5257
|
+
return { created: false, configPath };
|
|
5258
|
+
}
|
|
5259
|
+
const manifest = await readManifest(repoRoot);
|
|
5260
|
+
const devScript = readDevScript(manifest);
|
|
5261
|
+
const packageManager = await detectPackageManager(repoRoot, manifest);
|
|
5262
|
+
const document = {
|
|
5263
|
+
version: 1,
|
|
5264
|
+
project: {
|
|
5265
|
+
id: basename2(repoRoot),
|
|
5266
|
+
workspace: "."
|
|
5267
|
+
},
|
|
5268
|
+
gateway: {
|
|
5269
|
+
host: "0.0.0.0",
|
|
5270
|
+
port: "auto"
|
|
5271
|
+
},
|
|
5272
|
+
upstream: {
|
|
5273
|
+
port: "auto",
|
|
5274
|
+
command: devCommand(packageManager, devScript)
|
|
5275
|
+
}
|
|
5276
|
+
};
|
|
5277
|
+
await mkdir3(dirname3(configPath), { recursive: true });
|
|
5278
|
+
try {
|
|
5279
|
+
await writeFile3(configPath, stringifyYaml(document), {
|
|
5280
|
+
encoding: "utf8",
|
|
5281
|
+
flag: "wx"
|
|
5282
|
+
});
|
|
5283
|
+
} catch (error) {
|
|
5284
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
5285
|
+
return { created: false, configPath };
|
|
5286
|
+
}
|
|
5287
|
+
throw error;
|
|
5288
|
+
}
|
|
5289
|
+
return { created: true, configPath, devScript, packageManager };
|
|
5290
|
+
}
|
|
5291
|
+
function formatInitResult(result) {
|
|
5292
|
+
if (!result.created) {
|
|
5293
|
+
return [
|
|
5294
|
+
`Already initialized: ${CONFIG_PATH}`,
|
|
5295
|
+
"Next: npx --yes visual-remote@latest dev"
|
|
5296
|
+
].join("\n");
|
|
5297
|
+
}
|
|
5298
|
+
return [
|
|
5299
|
+
`Created: ${CONFIG_PATH}`,
|
|
5300
|
+
`Detected: ${result.packageManager} dev (${result.devScript})`,
|
|
5301
|
+
"Next: npx --yes visual-remote@latest dev"
|
|
5302
|
+
].join("\n");
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5177
5305
|
// src/status.ts
|
|
5178
5306
|
async function getBridgeStatus(dependencies = {}) {
|
|
5179
5307
|
const repoRoot = await discoverGitWorktreeRoot(dependencies.cwd ?? process.cwd());
|
|
@@ -5224,10 +5352,28 @@ function setExitCode(dependencies, code) {
|
|
|
5224
5352
|
}
|
|
5225
5353
|
}
|
|
5226
5354
|
function createCli(dependencies = {}) {
|
|
5227
|
-
const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.1.
|
|
5228
|
-
program.command("
|
|
5229
|
-
|
|
5230
|
-
|
|
5355
|
+
const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.1.2");
|
|
5356
|
+
program.command("init").description("Create .visualdev/config.yaml for the current project").action(async () => {
|
|
5357
|
+
const result = await initializeVisualDev(dependencies);
|
|
5358
|
+
output(dependencies, formatInitResult(result));
|
|
5359
|
+
});
|
|
5360
|
+
program.command("attach [upstream]", { isDefault: true }).description("Attach the Bridge to an existing development server").option("--upstream <url>", "existing development server URL").option("--listen <port>", "gateway port (10001 or above)", parsePort).option("--host <host>", "gateway bind host (default: 0.0.0.0)").option("--public-url <url>", "public Gateway URL opened in the browser").action(
|
|
5361
|
+
async (upstream, options) => {
|
|
5362
|
+
const upstreamUrl = options.upstream ?? upstream;
|
|
5363
|
+
if (upstreamUrl === void 0) {
|
|
5364
|
+
throw new Error(
|
|
5365
|
+
"Provide the running app URL, for example: npx visual-remote http://localhost:9011"
|
|
5366
|
+
);
|
|
5367
|
+
}
|
|
5368
|
+
const bridge = await startAttachBridge(
|
|
5369
|
+
{
|
|
5370
|
+
upstream: upstreamUrl,
|
|
5371
|
+
...options.listen === void 0 ? {} : { listen: options.listen },
|
|
5372
|
+
...options.host === void 0 ? {} : { host: options.host },
|
|
5373
|
+
...options.publicUrl === void 0 ? {} : { publicUrl: options.publicUrl }
|
|
5374
|
+
},
|
|
5375
|
+
dependencies
|
|
5376
|
+
);
|
|
5231
5377
|
output(dependencies, formatBridgeSummary(bridge));
|
|
5232
5378
|
await runBridgeUntilSignal(bridge);
|
|
5233
5379
|
}
|
|
@@ -5271,7 +5417,9 @@ export {
|
|
|
5271
5417
|
formatBridgeStatus,
|
|
5272
5418
|
formatBridgeSummary,
|
|
5273
5419
|
formatDoctorChecks,
|
|
5420
|
+
formatInitResult,
|
|
5274
5421
|
getBridgeStatus,
|
|
5422
|
+
initializeVisualDev,
|
|
5275
5423
|
main,
|
|
5276
5424
|
runBridgeUntilSignal,
|
|
5277
5425
|
runDoctor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "visual-remote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Visual bridge from a running web UI to Codex in its Git worktree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"typescript": "^7.0.2",
|
|
44
44
|
"vitest": "^4.1.10",
|
|
45
45
|
"@visual-remote/bridge-core": "0.1.0",
|
|
46
|
-
"@visual-remote/
|
|
47
|
-
"@visual-remote/cli": "0.1.0",
|
|
46
|
+
"@visual-remote/protocol": "0.1.0",
|
|
48
47
|
"@visual-remote/overlay": "0.1.0",
|
|
49
|
-
"@visual-remote/
|
|
48
|
+
"@visual-remote/cli": "0.1.0",
|
|
49
|
+
"@visual-remote/gateway": "0.1.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "corepack pnpm run build:overlay && corepack pnpm run build:server",
|