@realsee/vr-cli 0.1.4 → 0.2.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/README.md +46 -29
- package/agent-assets/AGENTS.md +8 -5
- package/dist/cli.mjs +5 -5
- package/package.json +1 -1
- package/skills/realsee-vr-app/SKILL.md +1 -1
- package/skills/realsee-vr-app/references/dnalogel-openapi-recipes.md +90 -26
- package/skills/realsee-vr-app/references/five-runtime.md +2 -2
- package/skills/realsee-vr-app/scripts/verify-project.mjs +194 -13
- package/templates/base/.env.example +4 -2
- package/templates/base/.gitignore.template +2 -0
- package/templates/base/.prettierignore +2 -0
- package/templates/base/README.md +35 -8
- package/templates/base/deploy.json +11 -0
- package/templates/base/eslint.config.mjs +2 -0
- package/templates/base/package-lock.json +11 -0
- package/templates/base/package.json +6 -1
- package/templates/base/scripts/package-vibe-release.mjs +569 -0
- package/templates/base/scripts/pairing.mjs +564 -0
- package/templates/base/scripts/pairing.test.mjs +227 -0
- package/templates/base/scripts/project-identity.mjs +160 -0
- package/templates/base/scripts/project-identity.test.mjs +119 -0
- package/templates/base/scripts/setup-config.mjs +252 -0
- package/templates/base/scripts/setup-config.test.mjs +123 -0
- package/templates/base/scripts/setup.mjs +203 -0
- package/templates/base/server/app.ts +3 -1
- package/templates/base/server/config.ts +12 -1
- package/templates/base/server/index.ts +5 -1
- package/templates/base/server/types.ts +2 -1
- package/templates/base/server/work-route.ts +51 -32
- package/templates/base/server/work-service.ts +2 -2
- package/templates/base/src/vr/work/WorkSource.ts +13 -9
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import {
|
|
3
|
+
chmod,
|
|
4
|
+
mkdir,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
readFile,
|
|
7
|
+
rm,
|
|
8
|
+
stat,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from 'node:fs/promises'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import test from 'node:test'
|
|
14
|
+
import {
|
|
15
|
+
CONTINUATION_RELATIVE_PATH,
|
|
16
|
+
readPairingContinuation,
|
|
17
|
+
runPairingCommand,
|
|
18
|
+
runPairingCompleteCommand,
|
|
19
|
+
} from './pairing.mjs'
|
|
20
|
+
|
|
21
|
+
const PORTAL_BASE_URL = 'http://127.0.0.1:43123'
|
|
22
|
+
const APP_KEY = 'test-app-key-1234'
|
|
23
|
+
const APP_SECRET = 'test-app-secret-value'
|
|
24
|
+
const ENV_KEYS = [
|
|
25
|
+
'REALSEE_PORTAL_BASE_URL',
|
|
26
|
+
'REALSEE_GATEWAY_APP_KEY',
|
|
27
|
+
'REALSEE_GATEWAY_APP_SECRET',
|
|
28
|
+
'REALSEE_DEFAULT_RESOURCE_CODE',
|
|
29
|
+
'REALSEE_GATEWAY_ENDPOINT',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
async function temporaryProject() {
|
|
33
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-pairing-'))
|
|
34
|
+
await writeFile(
|
|
35
|
+
path.join(root, 'package.json'),
|
|
36
|
+
JSON.stringify({ name: 'pairing-test-app' }),
|
|
37
|
+
)
|
|
38
|
+
return root
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function prepareProcess(t, root) {
|
|
42
|
+
const previousDirectory = process.cwd()
|
|
43
|
+
const previousEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]))
|
|
44
|
+
process.chdir(root)
|
|
45
|
+
for (const key of ENV_KEYS) delete process.env[key]
|
|
46
|
+
process.env.REALSEE_PORTAL_BASE_URL = PORTAL_BASE_URL
|
|
47
|
+
|
|
48
|
+
let stdout = ''
|
|
49
|
+
let stderr = ''
|
|
50
|
+
t.mock.method(process.stdout, 'write', (chunk) => {
|
|
51
|
+
stdout += String(chunk)
|
|
52
|
+
return true
|
|
53
|
+
})
|
|
54
|
+
t.mock.method(process.stderr, 'write', (chunk) => {
|
|
55
|
+
stderr += String(chunk)
|
|
56
|
+
return true
|
|
57
|
+
})
|
|
58
|
+
t.after(() => {
|
|
59
|
+
process.chdir(previousDirectory)
|
|
60
|
+
for (const [key, value] of previousEnv) {
|
|
61
|
+
if (value === undefined) delete process.env[key]
|
|
62
|
+
else process.env[key] = value
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
return {
|
|
66
|
+
stdout: () => stdout,
|
|
67
|
+
stderr: () => stderr,
|
|
68
|
+
clearOutput: () => {
|
|
69
|
+
stdout = ''
|
|
70
|
+
stderr = ''
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function jsonResponse(value) {
|
|
76
|
+
return new Response(JSON.stringify(value), {
|
|
77
|
+
status: 200,
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function installPortalFetch(t) {
|
|
83
|
+
const observed = { starts: [], polls: 0 }
|
|
84
|
+
let pollResult = 'denied'
|
|
85
|
+
t.mock.method(globalThis, 'fetch', async (input, options = {}) => {
|
|
86
|
+
const url = new URL(input)
|
|
87
|
+
if (url.pathname === '/api/cli/pairing/start') {
|
|
88
|
+
observed.starts.push(JSON.parse(options.body))
|
|
89
|
+
return jsonResponse({
|
|
90
|
+
ok: true,
|
|
91
|
+
data: {
|
|
92
|
+
user_code: 'GPVB-SWBF',
|
|
93
|
+
verification_uri_complete: `${PORTAL_BASE_URL}/spatial-apps/create/authorize?user_code=GPVB-SWBF`,
|
|
94
|
+
poll_secret: 'test-poll-secret',
|
|
95
|
+
interval: 5,
|
|
96
|
+
expires_in: 300,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
if (url.pathname === '/api/cli/pairing/poll') {
|
|
101
|
+
observed.polls += 1
|
|
102
|
+
assert.equal(options.headers.authorization, 'Bearer test-poll-secret')
|
|
103
|
+
if (pollResult === 'approved') {
|
|
104
|
+
return jsonResponse({
|
|
105
|
+
ok: true,
|
|
106
|
+
data: {
|
|
107
|
+
app_key: APP_KEY,
|
|
108
|
+
app_secret: APP_SECRET,
|
|
109
|
+
resource_code: 'resource-code-1',
|
|
110
|
+
gateway_endpoint: 'https://app-gateway.realsee.cn',
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
return jsonResponse({ ok: false, error: { code: 'access_denied' } })
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unexpected pairing request: ${url.pathname}`)
|
|
117
|
+
})
|
|
118
|
+
return {
|
|
119
|
+
observed,
|
|
120
|
+
approve: () => {
|
|
121
|
+
pollResult = 'approved'
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
test('completes the split pairing flow without printing credentials', async (t) => {
|
|
127
|
+
const root = await temporaryProject()
|
|
128
|
+
t.after(() => rm(root, { recursive: true, force: true }))
|
|
129
|
+
const output = prepareProcess(t, root)
|
|
130
|
+
const portal = installPortalFetch(t)
|
|
131
|
+
|
|
132
|
+
const started = await runPairingCommand({ noWait: true, json: true })
|
|
133
|
+
assert.equal(started, 2)
|
|
134
|
+
assert.equal(output.stderr(), '')
|
|
135
|
+
const pending = JSON.parse(output.stdout())
|
|
136
|
+
assert.equal(pending.ok, true)
|
|
137
|
+
assert.equal(pending.data.status, 'pending')
|
|
138
|
+
assert.equal(pending.data.user_code, 'GPVB-SWBF')
|
|
139
|
+
assert.match(pending.data.client_name, /^pairing-test-app .*#[0-9A-F]{8}$/u)
|
|
140
|
+
assert.doesNotMatch(output.stdout(), /test-poll-secret|test-app-secret/u)
|
|
141
|
+
assert.equal(portal.observed.starts.length, 1)
|
|
142
|
+
assert.equal(portal.observed.starts[0].environment, 'production')
|
|
143
|
+
assert.deepEqual(portal.observed.starts[0].capabilities, ['poll'])
|
|
144
|
+
|
|
145
|
+
const continuation = path.join(root, CONTINUATION_RELATIVE_PATH)
|
|
146
|
+
assert.equal((await stat(continuation)).mode & 0o777, 0o600)
|
|
147
|
+
assert.match(await readFile(continuation, 'utf8'), /test-poll-secret/u)
|
|
148
|
+
|
|
149
|
+
output.clearOutput()
|
|
150
|
+
portal.approve()
|
|
151
|
+
const completed = await runPairingCompleteCommand({ json: true })
|
|
152
|
+
assert.equal(completed, 0)
|
|
153
|
+
assert.equal(output.stderr(), '')
|
|
154
|
+
assert.deepEqual(JSON.parse(output.stdout()), {
|
|
155
|
+
ok: true,
|
|
156
|
+
data: {
|
|
157
|
+
status: 'paired',
|
|
158
|
+
app_key: 'tes****1234',
|
|
159
|
+
path: '.env.development',
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
assert.doesNotMatch(output.stdout(), /test-poll-secret|test-app-secret/u)
|
|
163
|
+
assert.equal(portal.observed.polls, 1)
|
|
164
|
+
|
|
165
|
+
const localConfig = path.join(root, '.env.development')
|
|
166
|
+
assert.equal((await stat(localConfig)).mode & 0o777, 0o600)
|
|
167
|
+
const config = await readFile(localConfig, 'utf8')
|
|
168
|
+
assert.match(config, /REALSEE_GATEWAY_APP_KEY="test-app-key-1234"/u)
|
|
169
|
+
assert.match(config, /REALSEE_GATEWAY_APP_SECRET="test-app-secret-value"/u)
|
|
170
|
+
assert.match(config, /REALSEE_DEFAULT_RESOURCE_CODE="resource-code-1"/u)
|
|
171
|
+
await assert.rejects(stat(continuation), { code: 'ENOENT' })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('a denied pairing preserves the existing local configuration', async (t) => {
|
|
175
|
+
const root = await temporaryProject()
|
|
176
|
+
t.after(() => rm(root, { recursive: true, force: true }))
|
|
177
|
+
const output = prepareProcess(t, root)
|
|
178
|
+
installPortalFetch(t)
|
|
179
|
+
const localConfig = path.join(root, '.env.development')
|
|
180
|
+
const existing = 'existing-local-config\n'
|
|
181
|
+
await writeFile(localConfig, existing)
|
|
182
|
+
if (process.platform !== 'win32') await chmod(localConfig, 0o600)
|
|
183
|
+
|
|
184
|
+
assert.equal(await runPairingCommand({ noWait: true, json: true }), 2)
|
|
185
|
+
output.clearOutput()
|
|
186
|
+
assert.equal(await runPairingCompleteCommand({ json: true }), 3)
|
|
187
|
+
assert.equal(output.stdout(), '')
|
|
188
|
+
assert.deepEqual(JSON.parse(output.stderr()), {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: {
|
|
191
|
+
code: 'access_denied',
|
|
192
|
+
message: '用户在 Portal 拒绝了本次配对请求。',
|
|
193
|
+
hint: '请勿自动重发;请与用户确认原因后重新运行 npm run setup。',
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
assert.equal(await readFile(localConfig, 'utf8'), existing)
|
|
197
|
+
await assert.rejects(stat(path.join(root, CONTINUATION_RELATIVE_PATH)), {
|
|
198
|
+
code: 'ENOENT',
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('rejects unsupported continuation schema versions', async () => {
|
|
203
|
+
const root = await temporaryProject()
|
|
204
|
+
try {
|
|
205
|
+
const directory = path.join(root, '.realsee')
|
|
206
|
+
await mkdir(directory, { mode: 0o700 })
|
|
207
|
+
await writeFile(
|
|
208
|
+
path.join(root, CONTINUATION_RELATIVE_PATH),
|
|
209
|
+
JSON.stringify({
|
|
210
|
+
schemaVersion: 999,
|
|
211
|
+
portalBaseUrl: 'https://open.realsee.cn',
|
|
212
|
+
userCode: 'GPVB-SWBF',
|
|
213
|
+
verificationUriComplete:
|
|
214
|
+
'https://open.realsee.cn/spatial-apps/create/authorize?user_code=GPVB-SWBF',
|
|
215
|
+
pollSecret: 'test-poll-secret',
|
|
216
|
+
intervalSeconds: 5,
|
|
217
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
await assert.rejects(
|
|
221
|
+
readPairingContinuation({ root }),
|
|
222
|
+
/不支持的 schemaVersion/u,
|
|
223
|
+
)
|
|
224
|
+
} finally {
|
|
225
|
+
await rm(root, { recursive: true, force: true })
|
|
226
|
+
}
|
|
227
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { lstat, mkdir, open, readFile } from 'node:fs/promises'
|
|
3
|
+
import { hostname, userInfo } from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const PROJECT_IDENTITY_RELATIVE_PATH = '.realsee/project.json'
|
|
7
|
+
// The integrated-commercial-cn pairing create API rejects client_name > 64 chars.
|
|
8
|
+
export const MAX_PAIRING_CLIENT_NAME_LENGTH = 64
|
|
9
|
+
|
|
10
|
+
const UUID_PATTERN =
|
|
11
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
|
|
12
|
+
|
|
13
|
+
function safeString(read) {
|
|
14
|
+
try {
|
|
15
|
+
const value = read()
|
|
16
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
17
|
+
} catch {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function displayPart(value, fallback) {
|
|
23
|
+
const cleaned = (typeof value === 'string' ? value : '')
|
|
24
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, '')
|
|
25
|
+
.replace(/[·#]/gu, ' ')
|
|
26
|
+
.replace(/\s+/gu, ' ')
|
|
27
|
+
.trim()
|
|
28
|
+
return cleaned || fallback
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readSavedIdentity(identityPath) {
|
|
32
|
+
const entry = await lstat(identityPath)
|
|
33
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
34
|
+
throw new Error(`${PROJECT_IDENTITY_RELATIVE_PATH} 必须是普通文件。`)
|
|
35
|
+
}
|
|
36
|
+
let identity
|
|
37
|
+
try {
|
|
38
|
+
identity = JSON.parse(await readFile(identityPath, 'utf8'))
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${PROJECT_IDENTITY_RELATIVE_PATH} 内容无效,请检查后再试。`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
const id = identity?.project_instance_id
|
|
45
|
+
if (
|
|
46
|
+
!identity ||
|
|
47
|
+
typeof identity !== 'object' ||
|
|
48
|
+
Array.isArray(identity) ||
|
|
49
|
+
Object.keys(identity).length !== 1 ||
|
|
50
|
+
typeof id !== 'string' ||
|
|
51
|
+
!UUID_PATTERN.test(id)
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(`${PROJECT_IDENTITY_RELATIVE_PATH} 缺少有效的项目实例 ID。`)
|
|
54
|
+
}
|
|
55
|
+
return id.toLowerCase()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getOrCreateProjectInstanceId(root = process.cwd()) {
|
|
59
|
+
const projectRoot = path.resolve(root)
|
|
60
|
+
const directory = path.join(projectRoot, '.realsee')
|
|
61
|
+
const identityPath = path.join(directory, 'project.json')
|
|
62
|
+
try {
|
|
63
|
+
await mkdir(directory, { mode: 0o700 })
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error?.code !== 'EEXIST') throw error
|
|
66
|
+
}
|
|
67
|
+
const directoryEntry = await lstat(directory)
|
|
68
|
+
if (!directoryEntry.isDirectory() || directoryEntry.isSymbolicLink()) {
|
|
69
|
+
throw new Error('.realsee 必须是当前项目内的普通目录。')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const id = randomUUID()
|
|
73
|
+
let file
|
|
74
|
+
try {
|
|
75
|
+
file = await open(identityPath, 'wx', 0o600)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code !== 'EEXIST') throw error
|
|
78
|
+
// A concurrent setup may have created the file but not finished writing it.
|
|
79
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
80
|
+
try {
|
|
81
|
+
return await readSavedIdentity(identityPath)
|
|
82
|
+
} catch (readError) {
|
|
83
|
+
if (attempt === 9 || !String(readError?.message).includes('内容无效')) {
|
|
84
|
+
throw readError
|
|
85
|
+
}
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await file.writeFile(
|
|
92
|
+
`${JSON.stringify({ project_instance_id: id }, null, 2)}\n`,
|
|
93
|
+
'utf8',
|
|
94
|
+
)
|
|
95
|
+
await file.sync()
|
|
96
|
+
} finally {
|
|
97
|
+
await file.close()
|
|
98
|
+
}
|
|
99
|
+
return id
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function readProjectName(root = process.cwd()) {
|
|
103
|
+
const projectRoot = path.resolve(root)
|
|
104
|
+
try {
|
|
105
|
+
const manifest = JSON.parse(
|
|
106
|
+
await readFile(path.join(projectRoot, 'package.json'), 'utf8'),
|
|
107
|
+
)
|
|
108
|
+
if (typeof manifest?.name === 'string') {
|
|
109
|
+
return displayPart(
|
|
110
|
+
manifest.name,
|
|
111
|
+
displayPart(path.basename(projectRoot), 'VR App'),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// An absent or invalid manifest must not turn a non-sensitive display label
|
|
116
|
+
// into a blocker for an otherwise valid pairing request.
|
|
117
|
+
}
|
|
118
|
+
return displayPart(path.basename(projectRoot), 'VR App')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatPairingClientName({
|
|
122
|
+
projectName,
|
|
123
|
+
projectInstanceId,
|
|
124
|
+
user,
|
|
125
|
+
machine,
|
|
126
|
+
}) {
|
|
127
|
+
if (!UUID_PATTERN.test(projectInstanceId)) {
|
|
128
|
+
throw new Error('项目实例 ID 无效。')
|
|
129
|
+
}
|
|
130
|
+
const shortId = projectInstanceId.slice(0, 8).toUpperCase()
|
|
131
|
+
const suffix = ` · #${shortId}`
|
|
132
|
+
const available = MAX_PAIRING_CLIENT_NAME_LENGTH - suffix.length
|
|
133
|
+
const environment = `${displayPart(user, 'unknown')}@${displayPart(machine, 'unknown')}`
|
|
134
|
+
const limitedEnvironment = environment.slice(0, Math.min(32, available - 5))
|
|
135
|
+
const projectBudget = available - limitedEnvironment.length - 3
|
|
136
|
+
const project = displayPart(projectName, 'VR App').slice(0, projectBudget)
|
|
137
|
+
return `${project} · ${limitedEnvironment}${suffix}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function currentProjectClientName(
|
|
141
|
+
root = process.cwd(),
|
|
142
|
+
env = process.env,
|
|
143
|
+
) {
|
|
144
|
+
const [projectName, projectInstanceId] = await Promise.all([
|
|
145
|
+
readProjectName(root),
|
|
146
|
+
getOrCreateProjectInstanceId(root),
|
|
147
|
+
])
|
|
148
|
+
const user =
|
|
149
|
+
safeString(() => userInfo().username) ||
|
|
150
|
+
env.USER?.trim() ||
|
|
151
|
+
env.USERNAME?.trim() ||
|
|
152
|
+
'unknown'
|
|
153
|
+
const machine = safeString(hostname) || 'unknown'
|
|
154
|
+
return formatPairingClientName({
|
|
155
|
+
projectName,
|
|
156
|
+
projectInstanceId,
|
|
157
|
+
user,
|
|
158
|
+
machine,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import {
|
|
3
|
+
mkdtemp,
|
|
4
|
+
readFile,
|
|
5
|
+
rm,
|
|
6
|
+
stat,
|
|
7
|
+
symlink,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from 'node:fs/promises'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import test from 'node:test'
|
|
13
|
+
import {
|
|
14
|
+
currentProjectClientName,
|
|
15
|
+
formatPairingClientName,
|
|
16
|
+
getOrCreateProjectInstanceId,
|
|
17
|
+
MAX_PAIRING_CLIENT_NAME_LENGTH,
|
|
18
|
+
readProjectName,
|
|
19
|
+
} from './project-identity.mjs'
|
|
20
|
+
|
|
21
|
+
async function temporaryProject(name = 'my-vr-app') {
|
|
22
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-project-id-'))
|
|
23
|
+
await writeFile(path.join(root, 'package.json'), JSON.stringify({ name }))
|
|
24
|
+
return root
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('keeps one local project ID across repeated pairing attempts', async () => {
|
|
28
|
+
const root = await temporaryProject()
|
|
29
|
+
try {
|
|
30
|
+
const first = await getOrCreateProjectInstanceId(root)
|
|
31
|
+
const second = await getOrCreateProjectInstanceId(root)
|
|
32
|
+
assert.equal(second, first)
|
|
33
|
+
assert.match(first, /^[0-9a-f]{8}-[0-9a-f-]{27}$/u)
|
|
34
|
+
const identityPath = path.join(root, '.realsee', 'project.json')
|
|
35
|
+
assert.deepEqual(JSON.parse(await readFile(identityPath, 'utf8')), {
|
|
36
|
+
project_instance_id: first,
|
|
37
|
+
})
|
|
38
|
+
if (process.platform !== 'win32') {
|
|
39
|
+
assert.equal((await stat(identityPath)).mode & 0o777, 0o600)
|
|
40
|
+
}
|
|
41
|
+
assert.match(
|
|
42
|
+
await currentProjectClientName(root),
|
|
43
|
+
/my-vr-app.*#[0-9A-F]{8}$/u,
|
|
44
|
+
)
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(root, { recursive: true, force: true })
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('distinguishes same-name projects without sharing local configuration', async () => {
|
|
51
|
+
const firstRoot = await temporaryProject('same-name')
|
|
52
|
+
const secondRoot = await temporaryProject('same-name')
|
|
53
|
+
try {
|
|
54
|
+
const firstId = await getOrCreateProjectInstanceId(firstRoot)
|
|
55
|
+
const secondId = await getOrCreateProjectInstanceId(secondRoot)
|
|
56
|
+
assert.notEqual(firstId, secondId)
|
|
57
|
+
assert.notEqual(
|
|
58
|
+
formatPairingClientName({
|
|
59
|
+
projectName: 'same-name',
|
|
60
|
+
projectInstanceId: firstId,
|
|
61
|
+
user: 'a',
|
|
62
|
+
machine: 'm',
|
|
63
|
+
}),
|
|
64
|
+
formatPairingClientName({
|
|
65
|
+
projectName: 'same-name',
|
|
66
|
+
projectInstanceId: secondId,
|
|
67
|
+
user: 'a',
|
|
68
|
+
machine: 'm',
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(firstRoot, { recursive: true, force: true })
|
|
73
|
+
await rm(secondRoot, { recursive: true, force: true })
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('preserves the short ID while cleaning and bounding the display name', () => {
|
|
78
|
+
assert.equal(MAX_PAIRING_CLIENT_NAME_LENGTH, 64)
|
|
79
|
+
const id = '4ec413e0-a832-4c43-8958-7353bdf38df1'
|
|
80
|
+
const name = formatPairingClientName({
|
|
81
|
+
projectName: `${'长'.repeat(120)}\n·#`,
|
|
82
|
+
projectInstanceId: id,
|
|
83
|
+
user: 'user\u001b',
|
|
84
|
+
machine: 'machine'.repeat(20),
|
|
85
|
+
})
|
|
86
|
+
assert.ok(name.length <= MAX_PAIRING_CLIENT_NAME_LENGTH)
|
|
87
|
+
assert.match(name, / · #4EC413E0$/u)
|
|
88
|
+
assert.ok(!name.includes('\r'))
|
|
89
|
+
assert.ok(!name.includes('\n'))
|
|
90
|
+
assert.ok(!name.includes('\u001b'))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('uses only the directory name when package metadata is unavailable', async () => {
|
|
94
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-project-id-'))
|
|
95
|
+
try {
|
|
96
|
+
assert.equal(await readProjectName(root), path.basename(root))
|
|
97
|
+
await writeFile(path.join(root, 'package.json'), '{invalid')
|
|
98
|
+
assert.equal(await readProjectName(root), path.basename(root))
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(root, { recursive: true, force: true })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('rejects invalid or symlinked saved identities instead of replacing them', async () => {
|
|
105
|
+
const root = await temporaryProject()
|
|
106
|
+
try {
|
|
107
|
+
await getOrCreateProjectInstanceId(root)
|
|
108
|
+
const identityPath = path.join(root, '.realsee', 'project.json')
|
|
109
|
+
await writeFile(identityPath, '{invalid')
|
|
110
|
+
await assert.rejects(getOrCreateProjectInstanceId(root), /内容无效/u)
|
|
111
|
+
if (process.platform !== 'win32') {
|
|
112
|
+
await rm(identityPath)
|
|
113
|
+
await symlink(path.join(root, 'package.json'), identityPath)
|
|
114
|
+
await assert.rejects(getOrCreateProjectInstanceId(root), /普通文件/u)
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(root, { recursive: true, force: true })
|
|
118
|
+
}
|
|
119
|
+
})
|