@realsee/vr-cli 0.1.5 → 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 +45 -24
- package/agent-assets/AGENTS.md +7 -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 +186 -15
- package/templates/base/.env.example +2 -2
- package/templates/base/.gitignore.template +2 -0
- package/templates/base/.prettierignore +2 -0
- package/templates/base/README.md +32 -6
- 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 +4 -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 +96 -23
- package/templates/base/scripts/setup-config.test.mjs +49 -9
- package/templates/base/scripts/setup.mjs +161 -7
- package/templates/base/server/app.ts +1 -1
- package/templates/base/server/config.ts +5 -4
- package/templates/base/server/types.ts +1 -1
- package/templates/base/server/work-route.ts +14 -3
|
@@ -16,29 +16,59 @@ const CONFIG = Object.freeze({
|
|
|
16
16
|
appSecret: '测试-secret_+/=',
|
|
17
17
|
resourceCode: 'resource-code_1',
|
|
18
18
|
})
|
|
19
|
+
const CONFIG_WITHOUT_RESOURCE = Object.freeze({
|
|
20
|
+
appKey: CONFIG.appKey,
|
|
21
|
+
appSecret: CONFIG.appSecret,
|
|
22
|
+
})
|
|
19
23
|
|
|
20
24
|
const ENCODED =
|
|
21
25
|
'eyJhcHBLZXkiOiJ0ZXN0LWFwcC1rZXkiLCJhcHBTZWNyZXQiOiLmtYvor5Utc2VjcmV0XysvPSIsInJlc291cmNlQ29kZSI6InJlc291cmNlLWNvZGVfMSJ9'
|
|
26
|
+
const encode = (value) =>
|
|
27
|
+
Buffer.from(JSON.stringify(value)).toString('base64url')
|
|
22
28
|
|
|
23
|
-
test('decodes the versioned
|
|
29
|
+
test('decodes the existing versioned payload with its optional resource fallback', () => {
|
|
24
30
|
assert.deepEqual(
|
|
25
31
|
decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}`),
|
|
26
32
|
CONFIG,
|
|
27
33
|
)
|
|
28
34
|
})
|
|
29
35
|
|
|
30
|
-
test('
|
|
36
|
+
test('accepts a v1 setup payload without a resource fallback', () => {
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
decodeSetupPayload(
|
|
39
|
+
`${SETUP_PAYLOAD_PREFIX}${encode(CONFIG_WITHOUT_RESOURCE)}`,
|
|
40
|
+
),
|
|
41
|
+
CONFIG_WITHOUT_RESOURCE,
|
|
42
|
+
)
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
decodeSetupPayload(
|
|
45
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG_WITHOUT_RESOURCE, resourceCode: '' })}`,
|
|
46
|
+
),
|
|
47
|
+
CONFIG_WITHOUT_RESOURCE,
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('rejects non-canonical, malformed and extra-field payloads', () => {
|
|
31
52
|
assert.throws(() => decodeSetupPayload('not-a-setup-payload'))
|
|
32
53
|
assert.throws(() => decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}=`))
|
|
33
|
-
const withExtraField = Buffer.from(
|
|
34
|
-
JSON.stringify({ ...CONFIG, unexpected: true }),
|
|
35
|
-
).toString('base64url')
|
|
36
54
|
assert.throws(() =>
|
|
37
|
-
decodeSetupPayload(
|
|
55
|
+
decodeSetupPayload(
|
|
56
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ appKey: CONFIG.appKey })}`,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
assert.throws(() =>
|
|
60
|
+
decodeSetupPayload(
|
|
61
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, resourceCode: 'invalid code' })}`,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
assert.throws(() =>
|
|
65
|
+
decodeSetupPayload(
|
|
66
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, unexpected: true })}`,
|
|
67
|
+
),
|
|
38
68
|
)
|
|
39
69
|
})
|
|
40
70
|
|
|
41
|
-
test('renders
|
|
71
|
+
test('renders the optional fallback only when it is configured', () => {
|
|
42
72
|
const rendered = renderLocalEnv(CONFIG)
|
|
43
73
|
assert.match(
|
|
44
74
|
rendered,
|
|
@@ -48,14 +78,24 @@ test('renders one server-only env file without printing the setup payload', () =
|
|
|
48
78
|
assert.match(rendered, /REALSEE_GATEWAY_APP_SECRET="测试-secret_\+\/="/u)
|
|
49
79
|
assert.match(rendered, /REALSEE_DEFAULT_RESOURCE_CODE="resource-code_1"/u)
|
|
50
80
|
assert.doesNotMatch(rendered, /realsee-setup:v1:/u)
|
|
81
|
+
|
|
82
|
+
const withoutResource = renderLocalEnv(CONFIG_WITHOUT_RESOURCE)
|
|
83
|
+
assert.match(withoutResource, /REALSEE_GATEWAY_APP_KEY="test-app-key"/u)
|
|
84
|
+
assert.doesNotMatch(withoutResource, /REALSEE_DEFAULT_RESOURCE_CODE/u)
|
|
51
85
|
})
|
|
52
86
|
|
|
53
87
|
test('writes atomically with restrictive POSIX permissions', async () => {
|
|
54
88
|
if (process.platform === 'win32') return
|
|
55
89
|
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-setup-'))
|
|
56
90
|
try {
|
|
57
|
-
const target = await writeLocalConfig(
|
|
58
|
-
|
|
91
|
+
const target = await writeLocalConfig(CONFIG_WITHOUT_RESOURCE, {
|
|
92
|
+
root,
|
|
93
|
+
platform: 'linux',
|
|
94
|
+
})
|
|
95
|
+
assert.equal(
|
|
96
|
+
await readFile(target, 'utf8'),
|
|
97
|
+
renderLocalEnv(CONFIG_WITHOUT_RESOURCE),
|
|
98
|
+
)
|
|
59
99
|
assert.equal((await stat(target)).mode & 0o777, 0o600)
|
|
60
100
|
} finally {
|
|
61
101
|
await rm(root, { recursive: true, force: true })
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
2
3
|
import { Writable } from 'node:stream'
|
|
3
4
|
import { createInterface } from 'node:readline/promises'
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
decodeSetupPayload,
|
|
7
|
+
normalizeGatewayCredentials,
|
|
8
|
+
writeLocalConfig,
|
|
9
|
+
} from './setup-config.mjs'
|
|
10
|
+
import {
|
|
11
|
+
PAIRING_EXIT_CODES,
|
|
12
|
+
maskAppKey,
|
|
13
|
+
reportPairingFailure,
|
|
14
|
+
runPairingCommand,
|
|
15
|
+
runPairingCompleteCommand,
|
|
16
|
+
} from './pairing.mjs'
|
|
17
|
+
|
|
18
|
+
const HELP = `用法:npm run setup [-- <选项>]
|
|
19
|
+
|
|
20
|
+
默认走配对:发起请求,打印验证链接与短码,阻塞轮询到终态并写入 .env.development。
|
|
21
|
+
--pair 显式选择配对路径(默认行为)。
|
|
22
|
+
--no-wait 发起后立即退出(退出码 2),稍后用 --complete 续做。
|
|
23
|
+
--complete 读取 .realsee/pairing-continuation.json,轮询到终态并写入。
|
|
24
|
+
--paste 回退路径:把 Portal 复制的单行配置包粘贴到隐藏输入。
|
|
25
|
+
--json 只输出 JSON 信封,不打印人类可读提示。
|
|
26
|
+
-h, --help 显示本帮助。
|
|
27
|
+
|
|
28
|
+
环境变量 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 优先级最高,
|
|
29
|
+
设置后跳过配对,直接写入 .env.development。
|
|
30
|
+
`
|
|
5
31
|
|
|
6
32
|
class HiddenOutput extends Writable {
|
|
7
33
|
_write(_chunk, _encoding, callback) {
|
|
@@ -33,17 +59,145 @@ async function readHiddenPayload() {
|
|
|
33
59
|
}
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
|
|
62
|
+
async function runPasteFlow() {
|
|
37
63
|
const payload = decodeSetupPayload(await readHiddenPayload())
|
|
38
64
|
await writeLocalConfig(payload)
|
|
39
65
|
console.log('配置完成。请回到 AI 助手并回复“已完成”。')
|
|
40
66
|
}
|
|
41
67
|
|
|
68
|
+
function parseSetupArguments(argv) {
|
|
69
|
+
try {
|
|
70
|
+
return parseArgs({
|
|
71
|
+
args: argv,
|
|
72
|
+
allowPositionals: false,
|
|
73
|
+
strict: true,
|
|
74
|
+
options: {
|
|
75
|
+
pair: { type: 'boolean', default: false },
|
|
76
|
+
paste: { type: 'boolean', default: false },
|
|
77
|
+
complete: { type: 'boolean', default: false },
|
|
78
|
+
'no-wait': { type: 'boolean', default: false },
|
|
79
|
+
json: { type: 'boolean', default: false },
|
|
80
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
81
|
+
},
|
|
82
|
+
}).values
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`参数错误:${error instanceof Error ? error.message : String(error)}`,
|
|
86
|
+
{ cause: error },
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveMode(values) {
|
|
92
|
+
if (values.paste) {
|
|
93
|
+
const conflicting = ['pair', 'complete', 'no-wait', 'json'].find(
|
|
94
|
+
(flag) => values[flag] === true,
|
|
95
|
+
)
|
|
96
|
+
if (conflicting) {
|
|
97
|
+
throw new Error(`--paste 不能与 --${conflicting} 同时使用。`)
|
|
98
|
+
}
|
|
99
|
+
return 'paste'
|
|
100
|
+
}
|
|
101
|
+
if (values.complete) {
|
|
102
|
+
if (values['no-wait']) {
|
|
103
|
+
throw new Error('--complete 不能与 --no-wait 同时使用。')
|
|
104
|
+
}
|
|
105
|
+
return 'complete'
|
|
106
|
+
}
|
|
107
|
+
return 'pair'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function environmentCredentials(env = process.env) {
|
|
111
|
+
const appKey = env.REALSEE_GATEWAY_APP_KEY?.trim()
|
|
112
|
+
const appSecret = env.REALSEE_GATEWAY_APP_SECRET?.trim()
|
|
113
|
+
if (!appKey && !appSecret) return null
|
|
114
|
+
if (!appKey || !appSecret) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'REALSEE_GATEWAY_APP_KEY 与 REALSEE_GATEWAY_APP_SECRET 必须同时提供。',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const resourceCode = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
|
|
120
|
+
const gatewayEndpoint = env.REALSEE_GATEWAY_ENDPOINT?.trim()
|
|
121
|
+
return normalizeGatewayCredentials({
|
|
122
|
+
appKey,
|
|
123
|
+
appSecret,
|
|
124
|
+
...(resourceCode ? { resourceCode } : {}),
|
|
125
|
+
...(gatewayEndpoint ? { gatewayEndpoint } : {}),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function configureFromEnvironment(config, { json }) {
|
|
130
|
+
await writeLocalConfig(config)
|
|
131
|
+
if (json !== true) {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
'检测到 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 环境变量(优先级最高),已跳过配对并写入 .env.development。\n',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
`${JSON.stringify({
|
|
138
|
+
ok: true,
|
|
139
|
+
data: {
|
|
140
|
+
status: 'configured',
|
|
141
|
+
source: 'environment',
|
|
142
|
+
app_key: maskAppKey(config.appKey),
|
|
143
|
+
path: '.env.development',
|
|
144
|
+
},
|
|
145
|
+
})}\n`,
|
|
146
|
+
)
|
|
147
|
+
return PAIRING_EXIT_CODES.OK
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
151
|
+
let values
|
|
152
|
+
try {
|
|
153
|
+
values = parseSetupArguments(argv)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return reportPairingFailure(error, { json: false })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (values.help) {
|
|
159
|
+
process.stdout.write(HELP)
|
|
160
|
+
return PAIRING_EXIT_CODES.OK
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let mode
|
|
164
|
+
try {
|
|
165
|
+
mode = resolveMode(values)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return reportPairingFailure(error, { json: values.json === true })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The paste fallback keeps its original interactive behaviour.
|
|
171
|
+
if (mode === 'paste') {
|
|
172
|
+
await runPasteFlow()
|
|
173
|
+
return PAIRING_EXIT_CODES.OK
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const json = values.json === true
|
|
177
|
+
try {
|
|
178
|
+
const config = environmentCredentials()
|
|
179
|
+
if (config) return await configureFromEnvironment(config, { json })
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return reportPairingFailure(error, { json })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mode === 'complete') {
|
|
185
|
+
return await runPairingCompleteCommand({ json })
|
|
186
|
+
}
|
|
187
|
+
return await runPairingCommand({ noWait: values['no-wait'] === true, json })
|
|
188
|
+
}
|
|
189
|
+
|
|
42
190
|
const entrypoint = process.argv[1]
|
|
43
191
|
if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
44
|
-
main()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
192
|
+
main()
|
|
193
|
+
.then((exitCode) => {
|
|
194
|
+
if (typeof exitCode === 'number' && exitCode !== 0) {
|
|
195
|
+
process.exitCode = exitCode
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.catch((error) => {
|
|
199
|
+
const message = error instanceof Error ? error.message : '未知错误。'
|
|
200
|
+
console.error(`本地配置失败:${message}`)
|
|
201
|
+
process.exitCode = 1
|
|
202
|
+
})
|
|
49
203
|
}
|
|
@@ -9,7 +9,7 @@ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
|
9
9
|
|
|
10
10
|
export interface CreateAppOptions {
|
|
11
11
|
readonly workService: WorkService
|
|
12
|
-
readonly defaultResourceCode: string
|
|
12
|
+
readonly defaultResourceCode: string | undefined
|
|
13
13
|
readonly logger?: boolean
|
|
14
14
|
readonly serveFrontend?: boolean
|
|
15
15
|
readonly frontendRoot?: string
|
|
@@ -21,11 +21,12 @@ function requiredCredential(env: ServerEnv, key: string): string {
|
|
|
21
21
|
return value
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function optionalResourceCode(env: ServerEnv): string | undefined {
|
|
25
25
|
const value = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
|
|
26
|
-
if (!nonEmptyString(value)
|
|
26
|
+
if (!nonEmptyString(value)) return undefined
|
|
27
|
+
if (!RESOURCE_CODE_PATTERN.test(value)) {
|
|
27
28
|
throw new Error(
|
|
28
|
-
'REALSEE_DEFAULT_RESOURCE_CODE
|
|
29
|
+
'REALSEE_DEFAULT_RESOURCE_CODE 格式无效,必须匹配 ^[A-Za-z0-9_-]{1,128}$。',
|
|
29
30
|
)
|
|
30
31
|
}
|
|
31
32
|
return value
|
|
@@ -59,6 +60,6 @@ export function loadServerConfig({
|
|
|
59
60
|
gatewayEndpoint: gatewayEndpoint.href.replace(/\/$/, ''),
|
|
60
61
|
appKey: requiredCredential(env, 'REALSEE_GATEWAY_APP_KEY'),
|
|
61
62
|
appSecret: requiredCredential(env, 'REALSEE_GATEWAY_APP_SECRET'),
|
|
62
|
-
defaultResourceCode:
|
|
63
|
+
defaultResourceCode: optionalResourceCode(env),
|
|
63
64
|
})
|
|
64
65
|
}
|
|
@@ -11,7 +11,7 @@ export interface ServerConfig {
|
|
|
11
11
|
readonly gatewayEndpoint: string
|
|
12
12
|
readonly appKey: string
|
|
13
13
|
readonly appSecret: string
|
|
14
|
-
readonly defaultResourceCode: string
|
|
14
|
+
readonly defaultResourceCode: string | undefined
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface RequestOptions {
|
|
@@ -5,7 +5,7 @@ const RESOURCE_CODE_PATTERN = '^[A-Za-z0-9_-]{1,128}$'
|
|
|
5
5
|
|
|
6
6
|
export interface WorkRoutesOptions {
|
|
7
7
|
readonly workService: WorkService
|
|
8
|
-
readonly defaultResourceCode: string
|
|
8
|
+
readonly defaultResourceCode: string | undefined
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface WorkRouteParams {
|
|
@@ -59,8 +59,19 @@ export function workRoutes(
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
app.get(
|
|
63
|
-
|
|
62
|
+
app.get(
|
|
63
|
+
'/api/work',
|
|
64
|
+
async (request, reply): Promise<WorkEnvelope | FastifyReply> => {
|
|
65
|
+
if (!defaultResourceCode) {
|
|
66
|
+
return reply.code(400).send({
|
|
67
|
+
error: 'RESOURCE_CODE_REQUIRED',
|
|
68
|
+
message:
|
|
69
|
+
'Provide resourceCode at /api/work/{resourceCode} or configure the optional REALSEE_DEFAULT_RESOURCE_CODE fallback.',
|
|
70
|
+
requestId: request.id,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return getWork(request, reply, defaultResourceCode)
|
|
74
|
+
},
|
|
64
75
|
)
|
|
65
76
|
|
|
66
77
|
app.get(
|