@nowcrew/daemon 0.6.88-beta.29 → 0.6.88-beta.30
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 +39 -1
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +8 -0
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/i18n.js +14 -0
- package/dist/message-file-preview.js +10 -3
- package/dist/server-url.js +25 -0
- package/dist/slog.js +2 -1
- package/dist/storage-runtime.js +2 -2
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -40,6 +40,43 @@ This compatibility form stays resident in the current terminal and reconnects wi
|
|
|
40
40
|
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
41
41
|
manual one-shot entry.
|
|
42
42
|
|
|
43
|
+
## Browser-Approved Computer Setup
|
|
44
|
+
|
|
45
|
+
The Web UI generates a token-free command for a specific Workspace and Machine:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx -y --registry https://registry.npmjs.org --package @nowcrew/daemon@latest nowwork-computer setup /nowcoder-app --machine <machine-uuid> --server-url https://nowwork.nowcoder.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`nowwork-computer setup` opens a one-time authorization page, prints the user code, polls until an owner or
|
|
52
|
+
admin approves it, then installs the selected daemon through the existing isolated profile and native service
|
|
53
|
+
installer. The command contains no long-lived `sk_machine_*` credential. The issued credential is held
|
|
54
|
+
only in memory until the private profile is written, and becomes permanent after successful finalization.
|
|
55
|
+
Each command run needs a new approval. Browser approval expires after ten minutes; credential exchange starts
|
|
56
|
+
a separate thirty-minute installation window. Failed installations restore the prior credential when still owned
|
|
57
|
+
by that setup. Interrupted setups are reconciled at server startup and every minute.
|
|
58
|
+
Only one unfinished credential exchange can own a Machine at a time; a concurrent setup receives a conflict.
|
|
59
|
+
Use `--no-browser` on hosts without a desktop browser and
|
|
60
|
+
`--json` for machine-readable status output. `--server-url` must be explicit for remote servers; it may also
|
|
61
|
+
come from `CREW_SERVER_URL`.
|
|
62
|
+
|
|
63
|
+
Alternatively, install both command entries first, then run setup:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install --global --registry https://registry.npmjs.org @nowcrew/daemon@latest
|
|
67
|
+
nowwork-computer setup --server-url https://nowwork.nowcoder.com /nowcoder-app --machine <machine-uuid>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With `CREW_SERVER_URL=https://nowwork.nowcoder.com` already set, the second command can be shortened to
|
|
71
|
+
`nowwork-computer setup /nowcoder-app --machine <machine-uuid>`. Replace the placeholder with the
|
|
72
|
+
Machine UUID from the Web command. New production setup links use `nowwork.nowcoder.com`;
|
|
73
|
+
the old `nowwork-in.nowcoder.com` hostname remains a production alias for existing profiles and pairing.
|
|
74
|
+
|
|
75
|
+
The setup flow validates the exact Workspace and Machine UUID and waits for the machine control-plane hello
|
|
76
|
+
to confirm Workspace, Machine, environment, profile, and storage identity. It does not migrate NAS or copy
|
|
77
|
+
existing `.crew` data. Existing `crew-daemon install --server-url ... --api-key ...` and profile lifecycle
|
|
78
|
+
commands remain available as compatibility and recovery entry points.
|
|
79
|
+
|
|
43
80
|
## Machine Environment
|
|
44
81
|
|
|
45
82
|
The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
|
|
@@ -72,7 +109,8 @@ npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
|
72
109
|
--release-tag beta --registry https://registry.npmjs.org
|
|
73
110
|
```
|
|
74
111
|
|
|
75
|
-
The installer selects the fixed
|
|
112
|
+
The installer selects the fixed production layout for both `nowwork.nowcoder.com` (canonical) and
|
|
113
|
+
`nowwork-in.nowcoder.com` (legacy compatibility), and the fixed `dev` layout for
|
|
76
114
|
`nowwork-dev.nowcoder.com`. It compares the parsed hostname exactly; lookalike suffixes are not accepted.
|
|
77
115
|
An unknown hostname is rejected before any write and reports how to retry with an explicit custom profile:
|
|
78
116
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { posix, win32 } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
4
5
|
import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
|
|
5
6
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
@@ -14,6 +15,8 @@ import { daemonRegistryCandidates } from "./daemon-registry.js";
|
|
|
14
15
|
import { npmInvocation } from "./npm-command.js";
|
|
15
16
|
import { discoverExactLegacyService, } from "./automatic-service-migration.js";
|
|
16
17
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
18
|
+
import { serverHostIdentity } from "./server-url.js";
|
|
19
|
+
import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
|
|
17
20
|
function profileStorage(platform, userHome, runner) {
|
|
18
21
|
return {
|
|
19
22
|
platform,
|
|
@@ -27,7 +30,7 @@ function profileStorage(platform, userHome, runner) {
|
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
function sameHostname(left, right) {
|
|
30
|
-
return
|
|
33
|
+
return serverHostIdentity(left) === serverHostIdentity(right);
|
|
31
34
|
}
|
|
32
35
|
function localHomeForTarget(target, platform) {
|
|
33
36
|
const paths = platform === "win32" ? win32 : posix;
|
|
@@ -246,8 +249,33 @@ function recoveryError(primary, failures) {
|
|
|
246
249
|
return original;
|
|
247
250
|
return new Error(`${original.message}; recovery failed: ${failures.map((error) => error.message).join("; ")}`, { cause: original });
|
|
248
251
|
}
|
|
252
|
+
function setupIdentity(input) {
|
|
253
|
+
if (input.workspaceId === undefined && input.machineId === undefined)
|
|
254
|
+
return null;
|
|
255
|
+
if (input.workspaceId === undefined || input.machineId === undefined) {
|
|
256
|
+
throw new Error("Computer setup requires both Workspace and Machine identity");
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
workspaceId: z.string().uuid().parse(input.workspaceId),
|
|
260
|
+
machineId: z.string().uuid().parse(input.machineId),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function assertExistingProfileIdentity(previous, identity) {
|
|
264
|
+
if (previous === null || identity === null)
|
|
265
|
+
return;
|
|
266
|
+
if (previous.workspaceId === undefined || previous.machineId === undefined) {
|
|
267
|
+
throw new Error(`Profile '${previous.name}' does not record its Workspace and Machine identity`);
|
|
268
|
+
}
|
|
269
|
+
if (previous.workspaceId !== identity.workspaceId) {
|
|
270
|
+
throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
|
|
271
|
+
}
|
|
272
|
+
if (previous.machineId !== identity.machineId) {
|
|
273
|
+
throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
249
276
|
export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
|
|
250
277
|
const machineToken = MachineTokenSchema.parse(input.machineToken);
|
|
278
|
+
const identity = setupIdentity(input);
|
|
251
279
|
const requestedLocalHome = input.requestedLocalHome ?? dependencies.environment.NOWWORK_LOCAL_HOME;
|
|
252
280
|
const requestedLocalHomeMode = input.requestedLocalHomeMode
|
|
253
281
|
?? dependencies.environment.NOWWORK_LOCAL_HOME_MODE
|
|
@@ -280,6 +308,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
280
308
|
});
|
|
281
309
|
const previousProfile = await dependencies.captureProfile(target);
|
|
282
310
|
await dependencies.assertProfileTarget(target, previousProfile);
|
|
311
|
+
assertExistingProfileIdentity(previousProfile, identity);
|
|
283
312
|
const previousService = await dependencies.captureService(spec, target);
|
|
284
313
|
if (previousService.installed && !previousService.exact) {
|
|
285
314
|
throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
|
|
@@ -330,6 +359,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
330
359
|
name: target.profile,
|
|
331
360
|
serverUrl: target.serverUrl,
|
|
332
361
|
machineToken,
|
|
362
|
+
...(identity === null ? {} : identity),
|
|
333
363
|
agentsRoot: target.agentsRoot,
|
|
334
364
|
releaseTag: target.releaseTag,
|
|
335
365
|
...(target.registryUrl === undefined ? {} : { registryUrl: target.registryUrl }),
|
|
@@ -341,14 +371,33 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
341
371
|
const status = await dependencies.verifyService(spec);
|
|
342
372
|
if (!status.running)
|
|
343
373
|
throw new Error(`Service '${spec.id}' did not reach running state`);
|
|
344
|
-
|
|
374
|
+
const result = {
|
|
345
375
|
profile: target.profile,
|
|
346
376
|
version: dependencies.version,
|
|
347
377
|
serviceId: spec.id,
|
|
348
378
|
running: true,
|
|
379
|
+
localHome: localHomeForTarget(target, dependencies.platform),
|
|
380
|
+
daemonHome: target.daemonHome,
|
|
381
|
+
agentsRoot: target.agentsRoot,
|
|
349
382
|
};
|
|
383
|
+
await input.verifyAfterStart?.(result);
|
|
384
|
+
return result;
|
|
350
385
|
}
|
|
351
386
|
catch (error) {
|
|
387
|
+
try {
|
|
388
|
+
await input.beforeRollback?.();
|
|
389
|
+
}
|
|
390
|
+
catch (rollbackError) {
|
|
391
|
+
throw new Error(`${error.message}; local recovery blocked: ${rollbackError.message}. `
|
|
392
|
+
+ computerSetupRecoveryGuidance({
|
|
393
|
+
serverUrl: target.serverUrl,
|
|
394
|
+
workspaceId: input.workspaceId ?? "unknown",
|
|
395
|
+
machineId: input.machineId ?? "unknown",
|
|
396
|
+
profile: target.profile,
|
|
397
|
+
serviceId: spec.id,
|
|
398
|
+
daemonHome: target.daemonHome,
|
|
399
|
+
}), { cause: error });
|
|
400
|
+
}
|
|
352
401
|
const failures = [];
|
|
353
402
|
if (profileSaved) {
|
|
354
403
|
try {
|
|
@@ -4,6 +4,14 @@ import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
|
4
4
|
import { resolveDaemonLocalHome } from "./daemon-local-home.js";
|
|
5
5
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
6
6
|
const KNOWN_TARGETS = {
|
|
7
|
+
// Canonical production hostname. Keep the internal hostname below as a
|
|
8
|
+
// compatibility alias so existing profiles and service identities continue
|
|
9
|
+
// to work when users move between the two URLs.
|
|
10
|
+
"nowwork.nowcoder.com": {
|
|
11
|
+
environment: "production",
|
|
12
|
+
daemonDirectory: "daemon",
|
|
13
|
+
agentsDirectory: "agents",
|
|
14
|
+
},
|
|
7
15
|
"nowwork-in.nowcoder.com": {
|
|
8
16
|
environment: "production",
|
|
9
17
|
daemonDirectory: "daemon",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
3
|
+
import { runComputerSetupCommand, setupUsage } from "./computer-setup.js";
|
|
4
|
+
import { redactSetupSecrets } from "./computer-setup-client.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const result = await runComputerSetupCommand(process.argv.slice(2));
|
|
7
|
+
if (result !== null) {
|
|
8
|
+
process.exitCode = result;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const lang = detectDaemonLang();
|
|
12
|
+
process.stderr.write(`${formatDaemonText(lang, "Usage:")}\n${setupUsage((message, values) => formatDaemonText(lang, message, values))}`);
|
|
13
|
+
process.exitCode = 2;
|
|
14
|
+
}
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
process.stderr.write(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
});
|
package/dist/computer-profile.js
CHANGED
|
@@ -21,6 +21,8 @@ const ProfileFields = z.object({
|
|
|
21
21
|
name: z.string().regex(PROFILE_NAME),
|
|
22
22
|
serverUrl: ServerUrlSchema,
|
|
23
23
|
machineToken: MachineTokenSchema,
|
|
24
|
+
workspaceId: z.string().uuid().optional(),
|
|
25
|
+
machineId: z.string().uuid().optional(),
|
|
24
26
|
agentsRoot: z.string().min(1).optional(),
|
|
25
27
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
26
28
|
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
@@ -258,6 +260,8 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
258
260
|
version: 1,
|
|
259
261
|
name: profile.name,
|
|
260
262
|
serverUrl: profile.serverUrl,
|
|
263
|
+
...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
|
|
264
|
+
...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
|
|
261
265
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
262
266
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
263
267
|
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
@@ -302,6 +306,8 @@ export async function loadProfile(name, home = daemonHome(), options = {}) {
|
|
|
302
306
|
version: stored.version,
|
|
303
307
|
name: stored.name,
|
|
304
308
|
serverUrl: stored.serverUrl,
|
|
309
|
+
...(stored.workspaceId === undefined ? {} : { workspaceId: stored.workspaceId }),
|
|
310
|
+
...(stored.machineId === undefined ? {} : { machineId: stored.machineId }),
|
|
305
311
|
...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
|
|
306
312
|
...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
|
|
307
313
|
...(stored.releaseTag === undefined ? {} : { releaseTag: stored.releaseTag }),
|
|
@@ -388,6 +394,8 @@ export function publicProfile(profile) {
|
|
|
388
394
|
name: profile.name,
|
|
389
395
|
serverUrl: profile.serverUrl,
|
|
390
396
|
machineTokenConfigured: true,
|
|
397
|
+
...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
|
|
398
|
+
...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
|
|
391
399
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
392
400
|
};
|
|
393
401
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { MachineTokenSchema } from "./computer-profile.js";
|
|
4
|
+
import { systemCommandRunner, } from "./computer-service.js";
|
|
5
|
+
const HttpUrlSchema = z.string().url().refine((value) => {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
return (url.protocol === "http:" || url.protocol === "https:")
|
|
8
|
+
&& url.username.length === 0
|
|
9
|
+
&& url.password.length === 0;
|
|
10
|
+
}, "expected an HTTP(S) URL without embedded credentials");
|
|
11
|
+
const ServerOriginSchema = HttpUrlSchema.refine((value) => {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
return (url.pathname === "" || url.pathname === "/")
|
|
14
|
+
&& url.search.length === 0
|
|
15
|
+
&& url.hash.length === 0;
|
|
16
|
+
}, "expected a server origin without a path, query, or hash");
|
|
17
|
+
const SetupStartSchema = z.object({
|
|
18
|
+
deviceCode: z.string().min(16).max(512),
|
|
19
|
+
userCode: z.string().min(4).max(128),
|
|
20
|
+
verificationUri: HttpUrlSchema,
|
|
21
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
22
|
+
intervalSeconds: z.number().int().positive().max(300),
|
|
23
|
+
}).strict();
|
|
24
|
+
const SetupBootstrapSchema = z.object({
|
|
25
|
+
setupId: z.string().uuid(),
|
|
26
|
+
workspaceId: z.string().uuid(),
|
|
27
|
+
workspaceSlug: z.string().min(1).max(80).optional(),
|
|
28
|
+
machineId: z.string().uuid(),
|
|
29
|
+
serverUrl: ServerOriginSchema,
|
|
30
|
+
environment: z.string().min(1).max(64),
|
|
31
|
+
environmentId: z.string().min(1).max(128).optional(),
|
|
32
|
+
profile: z.string().min(1).max(128),
|
|
33
|
+
minimumConnectionEpoch: z.number().int().nonnegative(),
|
|
34
|
+
daemonVersion: z.string().min(1).max(128).optional(),
|
|
35
|
+
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
36
|
+
registryUrl: HttpUrlSchema.optional(),
|
|
37
|
+
storageId: z.string().min(1).max(256).optional(),
|
|
38
|
+
expiresAt: z.string().datetime({ offset: true }).optional(),
|
|
39
|
+
machineToken: MachineTokenSchema,
|
|
40
|
+
}).strict();
|
|
41
|
+
const SetupPendingSchema = z.object({
|
|
42
|
+
status: z.enum(["authorization_pending", "pending"]),
|
|
43
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
44
|
+
}).strict();
|
|
45
|
+
const SetupSlowDownSchema = z.object({
|
|
46
|
+
status: z.literal("slow_down"),
|
|
47
|
+
intervalSeconds: z.number().int().positive().max(300),
|
|
48
|
+
}).strict();
|
|
49
|
+
const SetupDeniedSchema = z.object({
|
|
50
|
+
status: z.enum(["access_denied", "denied"]),
|
|
51
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
52
|
+
}).strict();
|
|
53
|
+
const SetupExpiredSchema = z.object({
|
|
54
|
+
status: z.enum(["expired_token", "expired"]),
|
|
55
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
56
|
+
}).strict();
|
|
57
|
+
const SetupReadySchema = z.object({
|
|
58
|
+
status: z.literal("ready"),
|
|
59
|
+
machineId: z.string().uuid(),
|
|
60
|
+
workspaceId: z.string().min(1).max(256),
|
|
61
|
+
environment: z.string().min(1).max(64),
|
|
62
|
+
profile: z.string().min(1).max(128),
|
|
63
|
+
storageId: z.string().min(1).max(256).optional(),
|
|
64
|
+
daemonHome: z.string().min(1).max(32768),
|
|
65
|
+
agentsRoot: z.string().min(1).max(32768),
|
|
66
|
+
}).strict();
|
|
67
|
+
export class ComputerSetupHttpError extends Error {
|
|
68
|
+
status;
|
|
69
|
+
code;
|
|
70
|
+
constructor(message, status, code) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "ComputerSetupHttpError";
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function redactSetupSecrets(value, secrets = []) {
|
|
78
|
+
let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>");
|
|
79
|
+
for (const secret of secrets) {
|
|
80
|
+
if (secret.length > 0)
|
|
81
|
+
redacted = redacted.replaceAll(secret, "<redacted>");
|
|
82
|
+
}
|
|
83
|
+
return redacted;
|
|
84
|
+
}
|
|
85
|
+
export function computerSetupRecoveryGuidance(target) {
|
|
86
|
+
const localTarget = [
|
|
87
|
+
...(target.profile === undefined ? [] : [`profile '${target.profile}'`]),
|
|
88
|
+
...(target.serviceId === undefined ? [] : [`service '${target.serviceId}'`]),
|
|
89
|
+
...(target.daemonHome === undefined ? [] : [`daemon home '${target.daemonHome}'`]),
|
|
90
|
+
];
|
|
91
|
+
return `Local profile/service state was preserved${localTarget.length === 0 ? "" : ` for ${localTarget.join(", ")}`}. `
|
|
92
|
+
+ `Do not restore old credentials. Check Computer '${target.machineId}' in Workspace '${target.workspaceId}' `
|
|
93
|
+
+ `on ${target.serverUrl}. Resolve the reported error and check connectivity, then rerun nowwork-computer setup to reconcile the installation.`;
|
|
94
|
+
}
|
|
95
|
+
function normalizeServerUrl(value) {
|
|
96
|
+
const parsed = new URL(ServerOriginSchema.parse(value));
|
|
97
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
98
|
+
}
|
|
99
|
+
function pathForMachine(machineId, suffix) {
|
|
100
|
+
return `/api/machines/${encodeURIComponent(machineId)}/setup/${suffix}`;
|
|
101
|
+
}
|
|
102
|
+
function parseBootstrap(raw) {
|
|
103
|
+
const value = (raw !== null && typeof raw === "object" && "bootstrap" in raw)
|
|
104
|
+
? raw.bootstrap
|
|
105
|
+
: raw;
|
|
106
|
+
const candidate = value !== null && typeof value === "object"
|
|
107
|
+
? (() => {
|
|
108
|
+
const record = value;
|
|
109
|
+
const { token: legacyToken, ...rest } = record;
|
|
110
|
+
return {
|
|
111
|
+
...rest,
|
|
112
|
+
...(rest.machineToken === undefined && typeof legacyToken === "string"
|
|
113
|
+
? { machineToken: legacyToken }
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
})()
|
|
117
|
+
: value;
|
|
118
|
+
const parsed = SetupBootstrapSchema.parse(candidate);
|
|
119
|
+
return {
|
|
120
|
+
setupId: parsed.setupId,
|
|
121
|
+
workspaceId: parsed.workspaceId,
|
|
122
|
+
...(parsed.workspaceSlug === undefined ? {} : { workspaceSlug: parsed.workspaceSlug }),
|
|
123
|
+
machineId: parsed.machineId,
|
|
124
|
+
serverUrl: parsed.serverUrl,
|
|
125
|
+
environment: parsed.environment,
|
|
126
|
+
profile: parsed.profile,
|
|
127
|
+
minimumConnectionEpoch: parsed.minimumConnectionEpoch,
|
|
128
|
+
machineToken: parsed.machineToken,
|
|
129
|
+
...(parsed.environmentId === undefined ? {} : { environmentId: parsed.environmentId }),
|
|
130
|
+
...(parsed.daemonVersion === undefined ? {} : { daemonVersion: parsed.daemonVersion }),
|
|
131
|
+
...(parsed.releaseTag === undefined ? {} : { releaseTag: parsed.releaseTag }),
|
|
132
|
+
...(parsed.registryUrl === undefined ? {} : { registryUrl: parsed.registryUrl }),
|
|
133
|
+
...(parsed.storageId === undefined ? {} : { storageId: parsed.storageId }),
|
|
134
|
+
...(parsed.expiresAt === undefined ? {} : { expiresAt: parsed.expiresAt }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function parseStart(raw) {
|
|
138
|
+
return SetupStartSchema.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
function parsePoll(raw) {
|
|
141
|
+
if (raw !== null && typeof raw === "object" && "status" in raw
|
|
142
|
+
&& raw.status === "authorized") {
|
|
143
|
+
const record = raw;
|
|
144
|
+
if ("bootstrap" in raw) {
|
|
145
|
+
return { status: "authorized", bootstrap: parseBootstrap(raw) };
|
|
146
|
+
}
|
|
147
|
+
if (record.target === undefined || typeof record.token !== "string") {
|
|
148
|
+
throw new Error("Invalid authorized computer setup response");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
status: "authorized",
|
|
152
|
+
bootstrap: parseBootstrap({
|
|
153
|
+
...record.target,
|
|
154
|
+
setupId: record.setupId,
|
|
155
|
+
machineToken: record.token,
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const pending = SetupPendingSchema.safeParse(raw);
|
|
160
|
+
if (pending.success) {
|
|
161
|
+
return {
|
|
162
|
+
status: "authorization_pending",
|
|
163
|
+
intervalSeconds: pending.data.intervalSeconds ?? 5,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const slowDown = SetupSlowDownSchema.safeParse(raw);
|
|
167
|
+
if (slowDown.success)
|
|
168
|
+
return slowDown.data;
|
|
169
|
+
const denied = SetupDeniedSchema.safeParse(raw);
|
|
170
|
+
if (denied.success) {
|
|
171
|
+
return {
|
|
172
|
+
status: "access_denied",
|
|
173
|
+
intervalSeconds: denied.data.intervalSeconds ?? 5,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const expired = SetupExpiredSchema.safeParse(raw);
|
|
177
|
+
if (expired.success) {
|
|
178
|
+
return {
|
|
179
|
+
status: "expired_token",
|
|
180
|
+
intervalSeconds: expired.data.intervalSeconds ?? 5,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Invalid computer setup polling response");
|
|
184
|
+
}
|
|
185
|
+
function parseHello(raw) {
|
|
186
|
+
if (raw !== null && typeof raw === "object" && "helloConfirmed" in raw
|
|
187
|
+
&& raw.helloConfirmed === true) {
|
|
188
|
+
const value = raw;
|
|
189
|
+
const parsed = SetupReadySchema.parse({
|
|
190
|
+
status: "ready",
|
|
191
|
+
machineId: value.machineId,
|
|
192
|
+
workspaceId: value.workspaceId,
|
|
193
|
+
environment: value.environment,
|
|
194
|
+
profile: value.profile,
|
|
195
|
+
...(value.storageId === undefined ? {} : { storageId: value.storageId }),
|
|
196
|
+
...(value.daemonHome === undefined ? {} : { daemonHome: value.daemonHome }),
|
|
197
|
+
...(value.agentsRoot === undefined ? {} : { agentsRoot: value.agentsRoot }),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
status: "ready",
|
|
201
|
+
machineId: parsed.machineId,
|
|
202
|
+
workspaceId: parsed.workspaceId,
|
|
203
|
+
environment: parsed.environment,
|
|
204
|
+
profile: parsed.profile,
|
|
205
|
+
...(parsed.storageId === undefined ? {} : { storageId: parsed.storageId }),
|
|
206
|
+
daemonHome: parsed.daemonHome,
|
|
207
|
+
agentsRoot: parsed.agentsRoot,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const ready = SetupReadySchema.safeParse(raw);
|
|
211
|
+
if (ready.success) {
|
|
212
|
+
return {
|
|
213
|
+
status: "ready",
|
|
214
|
+
machineId: ready.data.machineId,
|
|
215
|
+
workspaceId: ready.data.workspaceId,
|
|
216
|
+
environment: ready.data.environment,
|
|
217
|
+
profile: ready.data.profile,
|
|
218
|
+
...(ready.data.storageId === undefined ? {} : { storageId: ready.data.storageId }),
|
|
219
|
+
daemonHome: ready.data.daemonHome,
|
|
220
|
+
agentsRoot: ready.data.agentsRoot,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const pending = z.object({
|
|
224
|
+
status: z.literal("pending"),
|
|
225
|
+
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
226
|
+
}).safeParse(raw);
|
|
227
|
+
if (pending.success)
|
|
228
|
+
return { status: "pending", intervalSeconds: pending.data.intervalSeconds ?? 2 };
|
|
229
|
+
throw new Error("Invalid computer setup hello response");
|
|
230
|
+
}
|
|
231
|
+
export function createComputerSetupClient(options) {
|
|
232
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
233
|
+
const serverUrl = normalizeServerUrl(options.serverUrl);
|
|
234
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 15_000;
|
|
235
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => {
|
|
236
|
+
setTimeout(resolve, milliseconds);
|
|
237
|
+
}));
|
|
238
|
+
const now = options.now ?? Date.now;
|
|
239
|
+
let activeMachineId;
|
|
240
|
+
const request = async (method, path, body, bearer) => {
|
|
241
|
+
const controller = new AbortController();
|
|
242
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetchImpl(`${serverUrl}${path}`, {
|
|
245
|
+
method,
|
|
246
|
+
headers: {
|
|
247
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
248
|
+
...(bearer === undefined ? {} : { authorization: `Bearer ${bearer}` }),
|
|
249
|
+
},
|
|
250
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
});
|
|
253
|
+
const payload = await response.json();
|
|
254
|
+
if (!response.ok || payload?.success === false) {
|
|
255
|
+
const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
|
|
256
|
+
throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
return (payload?.success === true ? payload.data : payload);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (controller.signal.aborted)
|
|
262
|
+
throw new Error("computer setup request timed out");
|
|
263
|
+
if (error instanceof ComputerSetupHttpError)
|
|
264
|
+
throw error;
|
|
265
|
+
throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
clearTimeout(timeoutId);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
start: async (input) => {
|
|
273
|
+
activeMachineId = input.machineId;
|
|
274
|
+
return parseStart(await request("POST", "/api/auth/computer-setup/start", {
|
|
275
|
+
...(input.workspace.kind === "id"
|
|
276
|
+
? { workspaceId: input.workspace.value }
|
|
277
|
+
: { workspaceSlug: input.workspace.value }),
|
|
278
|
+
machineId: input.machineId,
|
|
279
|
+
deviceName: (input.deviceName ?? hostname()).trim().slice(0, 120) || "computer",
|
|
280
|
+
serverUrl,
|
|
281
|
+
}));
|
|
282
|
+
},
|
|
283
|
+
poll: async (deviceCode) => {
|
|
284
|
+
if (activeMachineId === undefined)
|
|
285
|
+
throw new Error("computer setup polling has no active machine");
|
|
286
|
+
return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
|
|
287
|
+
},
|
|
288
|
+
waitForHello: async (input) => {
|
|
289
|
+
const timeoutMs = input.timeoutMs ?? 120_000;
|
|
290
|
+
const deadline = now() + timeoutMs;
|
|
291
|
+
for (;;) {
|
|
292
|
+
const status = parseHello(await request("GET", `/daemon/computer-setup/status?machine_id=${encodeURIComponent(input.machineId)}&workspace_id=${encodeURIComponent(input.workspaceId)}&profile=${encodeURIComponent(input.profile)}&setup_id=${encodeURIComponent(input.setupId)}&minimum_connection_epoch=${encodeURIComponent(String(input.minimumConnectionEpoch))}`, undefined, input.machineToken));
|
|
293
|
+
if (status.status === "ready")
|
|
294
|
+
return status;
|
|
295
|
+
if (now() >= deadline)
|
|
296
|
+
throw new Error("Timed out waiting for machine hello");
|
|
297
|
+
await sleep(Math.min(status.intervalSeconds * 1_000, Math.max(0, deadline - now())));
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
finalize: async (input) => {
|
|
301
|
+
const result = await request("POST", "/daemon/computer-setup/finalize", { setupId: input.setupId }, input.machineToken);
|
|
302
|
+
if (result?.finalized !== true)
|
|
303
|
+
throw new Error("computer setup credential finalization not confirmed");
|
|
304
|
+
},
|
|
305
|
+
rollback: async (input) => {
|
|
306
|
+
const result = await request("POST", "/daemon/computer-setup/rollback", { setupId: input.setupId }, input.machineToken);
|
|
307
|
+
return result.rolledBack === true;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export async function openSetupBrowser(url, platform = process.platform, runner = systemCommandRunner) {
|
|
312
|
+
const command = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
|
|
313
|
+
const args = [url];
|
|
314
|
+
const result = await runner(command, args);
|
|
315
|
+
if (result.exitCode !== 0) {
|
|
316
|
+
throw new Error(`Unable to open the computer setup browser: ${redactSetupSecrets(result.stderr.trim() || `exit ${result.exitCode}`)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { computerSetupRecoveryGuidance, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
2
|
+
import { isProductionServerHostname, serverUrlsMatch } from "./server-url.js";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { resolveAgentsRoot } from "./computer-profile.js";
|
|
5
|
+
function safeError(error, machineToken) {
|
|
6
|
+
const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
|
|
7
|
+
return new Error(message, error instanceof Error ? { cause: error } : undefined);
|
|
8
|
+
}
|
|
9
|
+
function assertBootstrapMatches(bootstrap, input) {
|
|
10
|
+
if (bootstrap.machineId !== input.args.machineId) {
|
|
11
|
+
throw new Error("setup bootstrap machine does not match --machine");
|
|
12
|
+
}
|
|
13
|
+
const workspaceMatches = input.args.workspace.kind === "id"
|
|
14
|
+
? input.args.workspace.value === bootstrap.workspaceId
|
|
15
|
+
: bootstrap.workspaceSlug !== undefined && input.args.workspace.value === bootstrap.workspaceSlug;
|
|
16
|
+
if (!workspaceMatches)
|
|
17
|
+
throw new Error("setup bootstrap Workspace does not match the requested Workspace");
|
|
18
|
+
const bootstrapUrl = new URL(bootstrap.serverUrl);
|
|
19
|
+
const requestedUrl = new URL(input.serverUrl);
|
|
20
|
+
if (!serverUrlsMatch(bootstrapUrl.origin, requestedUrl.origin)) {
|
|
21
|
+
throw new Error("setup bootstrap server does not match the requested server context");
|
|
22
|
+
}
|
|
23
|
+
const hostname = bootstrapUrl.hostname.toLowerCase();
|
|
24
|
+
const knownEnvironment = hostname === "nowwork-dev.nowcoder.com"
|
|
25
|
+
? "dev"
|
|
26
|
+
: isProductionServerHostname(hostname)
|
|
27
|
+
? "production"
|
|
28
|
+
: null;
|
|
29
|
+
if (knownEnvironment !== null) {
|
|
30
|
+
const expectedReleaseTag = knownEnvironment === "dev" ? "beta" : "latest";
|
|
31
|
+
if (bootstrap.environment !== knownEnvironment) {
|
|
32
|
+
throw new Error("setup bootstrap environment does not match the server hostname");
|
|
33
|
+
}
|
|
34
|
+
if (bootstrap.releaseTag !== undefined && bootstrap.releaseTag !== expectedReleaseTag) {
|
|
35
|
+
throw new Error("setup bootstrap release tag does not match the server hostname");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function secondsUntil(expiresAt, now) {
|
|
40
|
+
const remaining = Date.parse(expiresAt) - now();
|
|
41
|
+
return Math.max(0, Math.ceil(remaining / 1_000));
|
|
42
|
+
}
|
|
43
|
+
function localPathsMatch(left, right, platform) {
|
|
44
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
45
|
+
if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
|
|
46
|
+
return false;
|
|
47
|
+
const normalize = (value) => {
|
|
48
|
+
const resolved = resolveAgentsRoot(value, undefined, platform);
|
|
49
|
+
return platform === "win32" ? win32.normalize(resolved).toLocaleLowerCase("en-US") : resolved;
|
|
50
|
+
};
|
|
51
|
+
return normalize(left) === normalize(right);
|
|
52
|
+
}
|
|
53
|
+
async function obtainBootstrap(input, deps) {
|
|
54
|
+
const pairing = await deps.client.start({
|
|
55
|
+
workspace: input.args.workspace,
|
|
56
|
+
machineId: input.args.machineId,
|
|
57
|
+
});
|
|
58
|
+
deps.reportPairing(`Open ${pairing.verificationUri}`);
|
|
59
|
+
deps.reportPairing(`Code ${pairing.userCode}`);
|
|
60
|
+
try {
|
|
61
|
+
await deps.openBrowser(pairing.verificationUri);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
deps.reportPairing(`Browser open skipped: ${redactSetupSecrets(error.message)}`);
|
|
65
|
+
}
|
|
66
|
+
let intervalSeconds = pairing.intervalSeconds;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const remainingSeconds = secondsUntil(pairing.expiresAt, deps.now);
|
|
69
|
+
if (remainingSeconds <= 0)
|
|
70
|
+
throw new Error("computer setup pairing expired");
|
|
71
|
+
await deps.sleep(Math.min(intervalSeconds * 1_000, remainingSeconds * 1_000));
|
|
72
|
+
const result = await deps.client.poll(pairing.deviceCode);
|
|
73
|
+
if (result.status === "authorization_pending" || result.status === "slow_down") {
|
|
74
|
+
intervalSeconds = result.intervalSeconds;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (result.status === "access_denied")
|
|
78
|
+
throw new Error("computer setup pairing was denied");
|
|
79
|
+
if (result.status === "expired_token")
|
|
80
|
+
throw new Error("computer setup pairing expired");
|
|
81
|
+
return result.bootstrap;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function runComputerSetupOrchestration(input, deps) {
|
|
85
|
+
let machineToken;
|
|
86
|
+
let bootstrap;
|
|
87
|
+
let credentialFinalized = false;
|
|
88
|
+
let verifiedStorageId;
|
|
89
|
+
let rollbackAttempt;
|
|
90
|
+
const confirmRollback = () => {
|
|
91
|
+
// Reuse both success and failure so an outer catch cannot change the recovery decision.
|
|
92
|
+
rollbackAttempt ??= (async () => {
|
|
93
|
+
if (credentialFinalized)
|
|
94
|
+
throw new Error("computer setup credentials already finalized");
|
|
95
|
+
if (bootstrap === undefined || machineToken === undefined)
|
|
96
|
+
throw new Error("computer setup credentials unavailable");
|
|
97
|
+
for (let attempt = 0;; attempt += 1) {
|
|
98
|
+
try {
|
|
99
|
+
const confirmed = await deps.client.rollback({ setupId: bootstrap.setupId, machineToken });
|
|
100
|
+
if (confirmed !== true)
|
|
101
|
+
throw new Error("computer setup rollback not confirmed");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (attempt >= 2)
|
|
106
|
+
throw error;
|
|
107
|
+
await deps.sleep(1_000);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return rollbackAttempt;
|
|
112
|
+
};
|
|
113
|
+
try {
|
|
114
|
+
const authorized = await obtainBootstrap(input, deps);
|
|
115
|
+
const token = authorized.machineToken;
|
|
116
|
+
bootstrap = authorized;
|
|
117
|
+
machineToken = token;
|
|
118
|
+
assertBootstrapMatches(authorized, input);
|
|
119
|
+
const installation = await deps.installAutomatically({
|
|
120
|
+
serverUrl: authorized.serverUrl,
|
|
121
|
+
machineToken: token,
|
|
122
|
+
workspaceId: authorized.workspaceId,
|
|
123
|
+
machineId: authorized.machineId,
|
|
124
|
+
...(authorized.profile === undefined ? {} : { requestedProfile: authorized.profile }),
|
|
125
|
+
...(authorized.releaseTag === "latest" || authorized.releaseTag === "beta"
|
|
126
|
+
? { requestedReleaseTag: authorized.releaseTag }
|
|
127
|
+
: {}),
|
|
128
|
+
...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
|
|
129
|
+
beforeRollback: confirmRollback,
|
|
130
|
+
verifyAfterStart: async (candidate) => {
|
|
131
|
+
const hello = await deps.client.waitForHello({
|
|
132
|
+
machineToken: token,
|
|
133
|
+
machineId: authorized.machineId,
|
|
134
|
+
workspaceId: authorized.workspaceId,
|
|
135
|
+
profile: candidate.profile,
|
|
136
|
+
setupId: authorized.setupId,
|
|
137
|
+
minimumConnectionEpoch: authorized.minimumConnectionEpoch,
|
|
138
|
+
});
|
|
139
|
+
if (hello.machineId !== authorized.machineId)
|
|
140
|
+
throw new Error("machine hello identity mismatch");
|
|
141
|
+
if (hello.workspaceId !== authorized.workspaceId)
|
|
142
|
+
throw new Error("machine hello workspace identity mismatch");
|
|
143
|
+
if (hello.environment !== authorized.environment)
|
|
144
|
+
throw new Error("machine hello environment identity mismatch");
|
|
145
|
+
if (hello.profile !== candidate.profile)
|
|
146
|
+
throw new Error("machine hello profile identity mismatch");
|
|
147
|
+
const platform = deps.platform ?? process.platform;
|
|
148
|
+
if (!localPathsMatch(hello.daemonHome, candidate.daemonHome, platform))
|
|
149
|
+
throw new Error("machine hello daemon home mismatch");
|
|
150
|
+
if (!localPathsMatch(hello.agentsRoot, candidate.agentsRoot, platform))
|
|
151
|
+
throw new Error("machine hello agents root mismatch");
|
|
152
|
+
if (authorized.storageId !== undefined && hello.storageId !== authorized.storageId) {
|
|
153
|
+
throw new Error("machine hello storage identity mismatch");
|
|
154
|
+
}
|
|
155
|
+
verifiedStorageId = hello.storageId;
|
|
156
|
+
let finalizeError;
|
|
157
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
158
|
+
try {
|
|
159
|
+
await deps.client.finalize({ setupId: authorized.setupId, machineToken: token });
|
|
160
|
+
credentialFinalized = true;
|
|
161
|
+
finalizeError = undefined;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
finalizeError = error;
|
|
166
|
+
if (attempt < 2)
|
|
167
|
+
await deps.sleep(1_000);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!credentialFinalized) {
|
|
171
|
+
throw finalizeError ?? new Error("computer setup credential finalization failed");
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
workspaceId: authorized.workspaceId,
|
|
177
|
+
machineId: authorized.machineId,
|
|
178
|
+
environment: authorized.environment,
|
|
179
|
+
profile: installation.profile,
|
|
180
|
+
daemonVersion: installation.version,
|
|
181
|
+
serviceId: installation.serviceId,
|
|
182
|
+
serviceStatus: "running",
|
|
183
|
+
helloConfirmed: true,
|
|
184
|
+
...(authorized.environmentId === undefined ? {} : { environmentId: authorized.environmentId }),
|
|
185
|
+
...(verifiedStorageId === undefined ? {} : { storageId: verifiedStorageId }),
|
|
186
|
+
localNowworkHome: installation.localHome,
|
|
187
|
+
daemonHome: installation.daemonHome,
|
|
188
|
+
agentsRoot: installation.agentsRoot,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (bootstrap !== undefined && machineToken !== undefined && !credentialFinalized && rollbackAttempt === undefined) {
|
|
193
|
+
try {
|
|
194
|
+
await confirmRollback();
|
|
195
|
+
}
|
|
196
|
+
catch (rollbackError) {
|
|
197
|
+
error = new Error(`${error.message}; computer setup rollback failed: ${rollbackError.message}; `
|
|
198
|
+
+ `local recovery blocked. ${computerSetupRecoveryGuidance(bootstrap)}`, { cause: error });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw safeError(error, machineToken);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { DEFAULT_SERVER_URL } from "./config.js";
|
|
4
|
+
import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
|
|
5
|
+
import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
6
|
+
import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
|
|
7
|
+
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
8
|
+
import { systemCommandRunner } from "./computer-service.js";
|
|
9
|
+
const WorkspaceSlugSchema = z.string()
|
|
10
|
+
.min(2)
|
|
11
|
+
.max(40)
|
|
12
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u);
|
|
13
|
+
const WorkspaceIdSchema = z.string().uuid();
|
|
14
|
+
const MachineIdSchema = z.string().uuid();
|
|
15
|
+
const ServerUrlSchema = z.string().url().refine((value) => {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return (url.protocol === "http:" || url.protocol === "https:")
|
|
18
|
+
&& url.username.length === 0
|
|
19
|
+
&& url.password.length === 0
|
|
20
|
+
&& (url.pathname === "" || url.pathname === "/")
|
|
21
|
+
&& url.search.length === 0
|
|
22
|
+
&& url.hash.length === 0;
|
|
23
|
+
}, "server URL must be an HTTP(S) origin without embedded credentials");
|
|
24
|
+
function stringValue(values, key) {
|
|
25
|
+
const value = values[key];
|
|
26
|
+
return typeof value === "string" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function booleanValue(values, key) {
|
|
29
|
+
return values[key] === true;
|
|
30
|
+
}
|
|
31
|
+
function optionOccurrences(argv, name) {
|
|
32
|
+
const prefix = `--${name}`;
|
|
33
|
+
const values = [];
|
|
34
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
35
|
+
const value = argv[index];
|
|
36
|
+
if (value === prefix) {
|
|
37
|
+
const next = argv[index + 1];
|
|
38
|
+
if (next !== undefined && !next.startsWith("--"))
|
|
39
|
+
values.push(next);
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (value?.startsWith(`${prefix}=`))
|
|
44
|
+
values.push(value.slice(prefix.length + 1));
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
48
|
+
function assertRepeatedOptionValuesAgree(argv, name, normalize) {
|
|
49
|
+
const normalized = optionOccurrences(argv, name).map(normalize);
|
|
50
|
+
if (new Set(normalized).size > 1) {
|
|
51
|
+
throw new Error(`Repeated --${name} values conflict`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function normalizedServerOrigin(value) {
|
|
55
|
+
const parsed = new URL(ServerUrlSchema.parse(value));
|
|
56
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
57
|
+
}
|
|
58
|
+
function workspaceFromSlug(value) {
|
|
59
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
60
|
+
throw new Error("Workspace slug must use exactly one leading slash, for example /nowcoder-app");
|
|
61
|
+
}
|
|
62
|
+
return WorkspaceSlugSchema.parse(value.slice(1));
|
|
63
|
+
}
|
|
64
|
+
function workspaceFromAlias(value) {
|
|
65
|
+
return value.startsWith("/") ? workspaceFromSlug(value) : WorkspaceSlugSchema.parse(value);
|
|
66
|
+
}
|
|
67
|
+
export function parseSetupArgs(argv) {
|
|
68
|
+
assertRepeatedOptionValuesAgree(argv, "workspace", (value) => workspaceFromAlias(value));
|
|
69
|
+
assertRepeatedOptionValuesAgree(argv, "workspace-id", (value) => WorkspaceIdSchema.parse(value).toLowerCase());
|
|
70
|
+
assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
71
|
+
assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
72
|
+
assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
|
|
73
|
+
const parsed = parseArgs({
|
|
74
|
+
args: [...argv],
|
|
75
|
+
allowPositionals: true,
|
|
76
|
+
strict: true,
|
|
77
|
+
options: {
|
|
78
|
+
workspace: { type: "string" },
|
|
79
|
+
"workspace-id": { type: "string" },
|
|
80
|
+
machine: { type: "string" },
|
|
81
|
+
"machine-id": { type: "string" },
|
|
82
|
+
"server-url": { type: "string" },
|
|
83
|
+
json: { type: "boolean", default: false },
|
|
84
|
+
"no-browser": { type: "boolean", default: false },
|
|
85
|
+
help: { type: "boolean", short: "h", default: false },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const values = parsed.values;
|
|
89
|
+
const [command, ...positionals] = parsed.positionals;
|
|
90
|
+
if (command !== "setup")
|
|
91
|
+
throw new Error("Expected 'setup'");
|
|
92
|
+
if (booleanValue(values, "help"))
|
|
93
|
+
throw new Error(setupUsage());
|
|
94
|
+
if (positionals.length > 1)
|
|
95
|
+
throw new Error("Setup accepts one Workspace argument");
|
|
96
|
+
const workspaceAlias = stringValue(values, "workspace");
|
|
97
|
+
const workspaceIdAlias = stringValue(values, "workspace-id");
|
|
98
|
+
if (workspaceAlias !== undefined && workspaceIdAlias !== undefined) {
|
|
99
|
+
throw new Error("Choose one Workspace source: positional, --workspace, or --workspace-id");
|
|
100
|
+
}
|
|
101
|
+
const positionalWorkspace = positionals[0] === undefined ? undefined : workspaceFromSlug(positionals[0]);
|
|
102
|
+
const aliasWorkspace = workspaceAlias === undefined ? undefined : workspaceFromAlias(workspaceAlias);
|
|
103
|
+
if (positionalWorkspace !== undefined && workspaceIdAlias !== undefined) {
|
|
104
|
+
throw new Error("A positional Workspace slug cannot be combined with --workspace-id");
|
|
105
|
+
}
|
|
106
|
+
if (positionalWorkspace !== undefined && aliasWorkspace !== undefined && positionalWorkspace !== aliasWorkspace) {
|
|
107
|
+
throw new Error("Positional Workspace conflicts with --workspace");
|
|
108
|
+
}
|
|
109
|
+
const workspace = positionalWorkspace !== undefined
|
|
110
|
+
? Object.freeze({ kind: "slug", value: positionalWorkspace })
|
|
111
|
+
: aliasWorkspace !== undefined
|
|
112
|
+
? Object.freeze({ kind: "slug", value: aliasWorkspace })
|
|
113
|
+
: workspaceIdAlias === undefined
|
|
114
|
+
? (() => { throw new Error("Missing Workspace; use /<slug> or --workspace-id <uuid>"); })()
|
|
115
|
+
: Object.freeze({ kind: "id", value: WorkspaceIdSchema.parse(workspaceIdAlias).toLowerCase() });
|
|
116
|
+
const machineAlias = stringValue(values, "machine");
|
|
117
|
+
const machineIdAlias = stringValue(values, "machine-id");
|
|
118
|
+
const parsedMachine = MachineIdSchema.parse(machineAlias ?? machineIdAlias ?? (() => {
|
|
119
|
+
throw new Error("Missing --machine <uuid>");
|
|
120
|
+
})()).toLowerCase();
|
|
121
|
+
if (machineAlias !== undefined && machineIdAlias !== undefined
|
|
122
|
+
&& parsedMachine !== MachineIdSchema.parse(machineIdAlias).toLowerCase()) {
|
|
123
|
+
throw new Error("--machine conflicts with --machine-id");
|
|
124
|
+
}
|
|
125
|
+
const machineId = parsedMachine;
|
|
126
|
+
const serverUrl = stringValue(values, "server-url");
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
command: "setup",
|
|
129
|
+
workspace,
|
|
130
|
+
machineId,
|
|
131
|
+
...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
|
|
132
|
+
...(booleanValue(values, "json") ? { json: true } : {}),
|
|
133
|
+
...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function resolveSetupServerUrl(args, env) {
|
|
137
|
+
const configured = args.serverUrl ?? env.CREW_SERVER_URL?.trim();
|
|
138
|
+
if (configured !== undefined && configured.length > 0)
|
|
139
|
+
return normalizedServerOrigin(configured);
|
|
140
|
+
if (env.NODE_ENV === "development" || env.NODE_ENV === "test")
|
|
141
|
+
return DEFAULT_SERVER_URL;
|
|
142
|
+
throw new Error("server URL is required; pass --server-url or set CREW_SERVER_URL");
|
|
143
|
+
}
|
|
144
|
+
export function setupUsage(td = (message) => message) {
|
|
145
|
+
return [
|
|
146
|
+
td("Usage:"),
|
|
147
|
+
" nowwork-computer setup /workspace --machine <uuid>",
|
|
148
|
+
" nowwork-computer setup --workspace /workspace --machine <uuid>",
|
|
149
|
+
" nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
|
|
150
|
+
"",
|
|
151
|
+
"Options: --server-url <url> --json --no-browser",
|
|
152
|
+
].join("\n") + "\n";
|
|
153
|
+
}
|
|
154
|
+
function defaults() {
|
|
155
|
+
return {
|
|
156
|
+
env: process.env,
|
|
157
|
+
platform: process.platform,
|
|
158
|
+
fetch: globalThis.fetch,
|
|
159
|
+
stdout: (text) => process.stdout.write(text),
|
|
160
|
+
stderr: (text) => process.stderr.write(text),
|
|
161
|
+
createClient: (serverUrl) => createComputerSetupClient({ serverUrl }),
|
|
162
|
+
installAutomatically: installDaemonAutomatically,
|
|
163
|
+
openBrowser: (url) => openSetupBrowser(url, process.platform, systemCommandRunner),
|
|
164
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
165
|
+
now: Date.now,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function humanSummary(result, td) {
|
|
169
|
+
return [
|
|
170
|
+
td("Workspace: {{value}}", { value: result.workspaceId }),
|
|
171
|
+
td("Machine: {{value}}", { value: result.machineId }),
|
|
172
|
+
td("Environment: {{value}}", { value: result.environment }),
|
|
173
|
+
td("Daemon: profile {{profile}} / v{{version}} / {{status}}", {
|
|
174
|
+
profile: result.profile, version: result.daemonVersion, status: result.serviceStatus,
|
|
175
|
+
}),
|
|
176
|
+
td("Local: {{value}}", { value: result.localNowworkHome }),
|
|
177
|
+
td("Config: {{value}}", { value: result.daemonHome }),
|
|
178
|
+
td("Agents: {{value}}", { value: result.agentsRoot }),
|
|
179
|
+
...(result.storageId === undefined ? [] : [td("Storage ID: {{value}}", { value: result.storageId })]),
|
|
180
|
+
td(result.helloConfirmed ? "Status: connected and ready" : "Status: not ready"),
|
|
181
|
+
].join("\n") + "\n";
|
|
182
|
+
}
|
|
183
|
+
function localizedPairingMessage(lang, message) {
|
|
184
|
+
if (message.startsWith("Open ")) {
|
|
185
|
+
return formatDaemonText(lang, "Open {{url}}", { url: message.slice("Open ".length) });
|
|
186
|
+
}
|
|
187
|
+
if (message.startsWith("Code ")) {
|
|
188
|
+
return formatDaemonText(lang, "Code {{code}}", { code: message.slice("Code ".length) });
|
|
189
|
+
}
|
|
190
|
+
if (message.startsWith("Browser open skipped: ")) {
|
|
191
|
+
return formatDaemonText(lang, "Browser open skipped: {{reason}}", {
|
|
192
|
+
reason: message.slice("Browser open skipped: ".length),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return formatDaemonText(lang, message);
|
|
196
|
+
}
|
|
197
|
+
export async function runComputerSetupCommand(argv, overrides = {}) {
|
|
198
|
+
if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
|
|
199
|
+
const deps = { ...defaults(), ...overrides };
|
|
200
|
+
const lang = detectDaemonLang(deps.env);
|
|
201
|
+
deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
if (argv[0] !== "setup")
|
|
205
|
+
return null;
|
|
206
|
+
const deps = { ...defaults(), ...overrides };
|
|
207
|
+
const lang = detectDaemonLang(deps.env);
|
|
208
|
+
try {
|
|
209
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
210
|
+
deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
let args;
|
|
214
|
+
try {
|
|
215
|
+
args = parseSetupArgs(argv);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
if (args.json === true) {
|
|
222
|
+
deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
|
|
223
|
+
}
|
|
224
|
+
const serverUrl = resolveSetupServerUrl(args, deps.env);
|
|
225
|
+
const createClient = overrides.createClient
|
|
226
|
+
?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
|
|
227
|
+
const result = await runComputerSetupOrchestration({ args, serverUrl }, {
|
|
228
|
+
client: createClient(serverUrl),
|
|
229
|
+
installAutomatically: deps.installAutomatically,
|
|
230
|
+
openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
|
|
231
|
+
sleep: deps.sleep,
|
|
232
|
+
reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
|
|
233
|
+
now: deps.now,
|
|
234
|
+
});
|
|
235
|
+
deps.stdout(args.json === true
|
|
236
|
+
? `${JSON.stringify(result)}\n`
|
|
237
|
+
: humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
package/dist/i18n.js
CHANGED
|
@@ -31,6 +31,20 @@ const zh = {
|
|
|
31
31
|
"Run finished": "本轮结束",
|
|
32
32
|
"Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)": "缺少 CREW_MACHINE_TOKEN(sk_machine_*,由 seed 打印)",
|
|
33
33
|
"Usage:": "用法:",
|
|
34
|
+
"Open {{url}}": "打开 {{url}}",
|
|
35
|
+
"Code {{code}}": "验证码 {{code}}",
|
|
36
|
+
"Browser open skipped: {{reason}}": "未能自动打开浏览器:{{reason}}",
|
|
37
|
+
"Starting computer setup pairing...": "正在发起电脑接入授权…",
|
|
38
|
+
"Workspace: {{value}}": "工作区:{{value}}",
|
|
39
|
+
"Machine: {{value}}": "机器: {{value}}",
|
|
40
|
+
"Environment: {{value}}": "环境:{{value}}",
|
|
41
|
+
"Daemon: profile {{profile}} / v{{version}} / {{status}}": "Daemon:配置 {{profile}} / v{{version}} / {{status}}",
|
|
42
|
+
"Local: {{value}}": "本地目录:{{value}}",
|
|
43
|
+
"Config: {{value}}": "配置目录:{{value}}",
|
|
44
|
+
"Agents: {{value}}": "Agent 目录:{{value}}",
|
|
45
|
+
"Storage ID: {{value}}": "存储标识:{{value}}",
|
|
46
|
+
"Status: connected and ready": "状态:已连接并就绪",
|
|
47
|
+
"Status: not ready": "状态:尚未就绪",
|
|
34
48
|
"connect and stay resident": "连接并常驻",
|
|
35
49
|
"run once manually": "手动运行一次",
|
|
36
50
|
"resident, connecting to": "常驻,连接到",
|
|
@@ -10,6 +10,7 @@ import { isInternalEnvironmentPath } from "./workspace-fs.js";
|
|
|
10
10
|
import { opaqueKey, safeKey } from "./workspace.js";
|
|
11
11
|
import { AgentHandleSchema } from "./execution-protocol.js";
|
|
12
12
|
const MAX_BYTES = 256 * 1024;
|
|
13
|
+
const MAX_BINARY_BYTES = 8 * 1024 * 1024;
|
|
13
14
|
const MAX_CONCURRENT_READS = 4;
|
|
14
15
|
let activeReads = 0;
|
|
15
16
|
export const SharedFileRequestSchema = z.object({
|
|
@@ -182,7 +183,10 @@ async function readCandidate(candidate) {
|
|
|
182
183
|
if (current.dev !== opened.dev || current.ino !== opened.ino || !current.isFile() || current.nlink !== 1n) {
|
|
183
184
|
throw new SharedFileError("file_changed");
|
|
184
185
|
}
|
|
185
|
-
const
|
|
186
|
+
const knownBinary = /\.(?:xlsx|xls|pdf|png|jpe?g|gif|webp|zip|docx|pptx)$/iu.test(candidate.path);
|
|
187
|
+
if (knownBinary && opened.size > BigInt(MAX_BINARY_BYTES))
|
|
188
|
+
throw new SharedFileError("file_too_large");
|
|
189
|
+
const buffer = Buffer.alloc(knownBinary ? Number(opened.size) : MAX_BYTES);
|
|
186
190
|
let length = 0;
|
|
187
191
|
while (length < buffer.length) {
|
|
188
192
|
const read = await handle.read(buffer, length, buffer.length - length, length);
|
|
@@ -197,9 +201,12 @@ async function readCandidate(candidate) {
|
|
|
197
201
|
throw new SharedFileError("file_changed");
|
|
198
202
|
}
|
|
199
203
|
const bytes = buffer.subarray(0, length);
|
|
200
|
-
if (bytes.includes(0))
|
|
201
|
-
throw new SharedFileError("file_binary");
|
|
202
204
|
const truncated = opened.size > BigInt(length);
|
|
205
|
+
if (bytes.includes(0) || knownBinary) {
|
|
206
|
+
if (truncated)
|
|
207
|
+
throw new SharedFileError("file_too_large");
|
|
208
|
+
return { path: relative(candidate.root, candidate.path).split(sep).join("/"), content: bytes.toString("base64"), encoding: "base64", size: Number(opened.size), truncated };
|
|
209
|
+
}
|
|
203
210
|
let content;
|
|
204
211
|
try {
|
|
205
212
|
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: truncated });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CANONICAL_PRODUCTION_HOST = "nowwork.nowcoder.com";
|
|
2
|
+
const PRODUCTION_HOST_ALIASES = new Set([
|
|
3
|
+
CANONICAL_PRODUCTION_HOST,
|
|
4
|
+
"nowwork-in.nowcoder.com",
|
|
5
|
+
]);
|
|
6
|
+
function canonicalHostname(hostname) {
|
|
7
|
+
const normalized = hostname.toLowerCase();
|
|
8
|
+
return isProductionServerHostname(normalized) ? CANONICAL_PRODUCTION_HOST : normalized;
|
|
9
|
+
}
|
|
10
|
+
export function isProductionServerHostname(hostname) {
|
|
11
|
+
return PRODUCTION_HOST_ALIASES.has(hostname.toLowerCase());
|
|
12
|
+
}
|
|
13
|
+
/** Host identity used by profile/service guards; the public and internal production names are aliases. */
|
|
14
|
+
export function serverHostIdentity(serverUrl) {
|
|
15
|
+
return canonicalHostname(new URL(serverUrl).hostname);
|
|
16
|
+
}
|
|
17
|
+
/** URL identity used by storage records; preserve path/port/query while collapsing production host aliases. */
|
|
18
|
+
export function serverUrlIdentity(serverUrl) {
|
|
19
|
+
const parsed = new URL(serverUrl);
|
|
20
|
+
parsed.hostname = canonicalHostname(parsed.hostname);
|
|
21
|
+
return parsed.toString().replace(/\/+$/u, "");
|
|
22
|
+
}
|
|
23
|
+
export function serverUrlsMatch(left, right) {
|
|
24
|
+
return serverUrlIdentity(left) === serverUrlIdentity(right);
|
|
25
|
+
}
|
package/dist/slog.js
CHANGED
|
@@ -16,6 +16,7 @@ import { join, resolve } from "node:path";
|
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
17
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
18
18
|
import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
|
|
19
|
+
import { isProductionServerHostname } from "./server-url.js";
|
|
19
20
|
const FLUSH_INTERVAL_MS = 1500;
|
|
20
21
|
const BATCH_SIZE = 50;
|
|
21
22
|
const SPOOL_MAX_FILES = 50; // spool 目录文件数上限,超出丢最旧(日志非关键数据,防无限膨胀)
|
|
@@ -30,7 +31,7 @@ let lastErrorAt = 0;
|
|
|
30
31
|
let exitHookInstalled = false;
|
|
31
32
|
let localStateRoot;
|
|
32
33
|
function deploymentEnvironment(serverHost) {
|
|
33
|
-
if (serverHost
|
|
34
|
+
if (serverHost !== undefined && isProductionServerHostname(serverHost))
|
|
34
35
|
return "prod";
|
|
35
36
|
if (serverHost === "nowwork-dev.nowcoder.com")
|
|
36
37
|
return "dev";
|
package/dist/storage-runtime.js
CHANGED
|
@@ -2,12 +2,12 @@ import { findInstallationForProfile, readInstallations, resolveLocalStorageRoots
|
|
|
2
2
|
import { storageHelloIdentity } from "./storage-identity.js";
|
|
3
3
|
import { storageLayout } from "./storage-layout.js";
|
|
4
4
|
import { readStorageOwnershipClaim, resolveWorkspaceStorageOwnershipClaim, } from "./storage-ownership.js";
|
|
5
|
-
|
|
5
|
+
import { serverUrlsMatch } from "./server-url.js";
|
|
6
6
|
export function resolveStorageRuntime(input) {
|
|
7
7
|
const installation = findInstallationForProfile(input.records, input.profile);
|
|
8
8
|
if (installation === null)
|
|
9
9
|
return null;
|
|
10
|
-
if (
|
|
10
|
+
if (!serverUrlsMatch(installation.serverUrl, input.serverUrl)) {
|
|
11
11
|
throw new Error("installation server identity does not match the active profile");
|
|
12
12
|
}
|
|
13
13
|
const local = resolveLocalStorageRoots({
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.88-beta.
|
|
3
|
+
"version": "0.6.88-beta.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"daemon": "./dist/main.js",
|
|
9
|
+
"crew-daemon": "./dist/main.js",
|
|
10
|
+
"nowwork-computer": "./dist/computer-main.js"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"dist"
|