@nowcrew/daemon 0.6.98-beta.3 → 0.6.98
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 +1 -34
- package/dist/automatic-service-migration.js +6 -7
- package/dist/computer-main.js +0 -0
- package/dist/computer-service.js +42 -40
- package/dist/computer-setup-client.js +8 -22
- package/dist/computer-setup-orchestration.js +2 -13
- package/dist/computer-setup.js +6 -36
- package/dist/execution-runner.js +28 -16
- package/dist/external-output.js +12 -1
- package/dist/i18n.js +0 -10
- package/dist/local-executor.js +13 -6
- package/dist/machine-info.js +3 -3
- package/dist/main.js +0 -0
- package/dist/marketplace-controller.js +21 -8
- package/dist/marketplace-maintenance-bridge.js +79 -0
- package/dist/marketplace-maintenance-protocol.js +40 -0
- package/dist/marketplace-serve.js +4 -2
- package/dist/npm-command.js +5 -5
- package/dist/project-skills/machine-hello-publisher.js +6 -95
- package/dist/runtimes/pi.js +85 -17
- package/dist/serve.js +7 -54
- package/dist/windows-scheduled-task.js +3 -3
- package/package.json +8 -9
- package/dist/computer-token-setup.js +0 -46
- package/dist/powershell-command.js +0 -12
- package/dist/windows-task-principal.js +0 -26
package/README.md
CHANGED
|
@@ -40,40 +40,7 @@ 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
|
-
##
|
|
44
|
-
|
|
45
|
-
After the deployment enables Token setup, an owner/admin opens **Add Computer**, copies the complete
|
|
46
|
-
command, and pastes it into the target computer's terminal. The same steps apply on macOS, Linux/SSH,
|
|
47
|
-
and Windows. Node.js 22+ and npm must already be installed. The command launches the installer with npx;
|
|
48
|
-
no separate global installation, browser confirmation, or verification code is needed during execution.
|
|
49
|
-
|
|
50
|
-
The generated command pins an exact published installer version and includes a `--setup-token`.
|
|
51
|
-
This is an already-approved one-time connection request, valid for ten minutes. The program exchanges
|
|
52
|
-
it for the machine credential only when executed, then reuses native service installation, new-hello
|
|
53
|
-
verification, credential finalization and rollback. Merely generating or copying a command does not
|
|
54
|
-
rotate the existing credential. An expired or consumed command must be regenerated in the Web UI.
|
|
55
|
-
|
|
56
|
-
For a machine credential you already hold, the same setup entry accepts `--api-key` or
|
|
57
|
-
`CREW_MACHINE_TOKEN` without browser pairing:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
nowwork-computer setup --server-url https://nowwork.nowcoder.com /nowcoder-app \
|
|
61
|
-
--machine <machine-uuid> --api-key <existing-machine-token>
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
The machine Token must match the specified Workspace and Computer. This path never rotates the
|
|
65
|
-
credential or creates an approval request; it confirms a new control-plane connection with the expected
|
|
66
|
-
profile and paths before reporting success. Token sources cannot be combined. JSON output puts progress
|
|
67
|
-
on stderr and the final result on stdout. On Windows shells that block npm.ps1/npx.ps1, use npx.cmd as
|
|
68
|
-
the package entry; the setup arguments and workflow are unchanged.
|
|
69
|
-
|
|
70
|
-
Deployment requires `CREW_PUBLIC_URL` (or `CREW_SERVER_URL`) and
|
|
71
|
-
`CREW_COMPUTER_TOKEN_SETUP_VERSION` set to an exact, published version known to support the Token flags.
|
|
72
|
-
The server checks its published release catalog and channel before issuing a Token command. Leave the
|
|
73
|
-
version unset until that installer has been released; it returns `computer_token_setup_unavailable`
|
|
74
|
-
instead of generating a command for an unsupported package. Existing clients retain browser setup.
|
|
75
|
-
|
|
76
|
-
## Browser-Approved Computer Setup (Compatibility)
|
|
43
|
+
## Browser-Approved Computer Setup
|
|
77
44
|
|
|
78
45
|
The Web UI generates a token-free command for a specific Workspace and Machine:
|
|
79
46
|
|
|
@@ -3,8 +3,6 @@ import { posix, win32 } from "node:path";
|
|
|
3
3
|
import { buildServiceSpec, serviceStatus, windowsCommandArg, } from "./computer-service.js";
|
|
4
4
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
5
5
|
import { inspectLegacyServiceInstallationDetails } from "./legacy-service-installation.js";
|
|
6
|
-
import { powershellCommandArgs } from "./powershell-command.js";
|
|
7
|
-
import { windowsTaskPrincipalFields, windowsTaskPrincipalMatches, windowsTaskPrincipalScript, } from "./windows-task-principal.js";
|
|
8
6
|
function encoded(value) {
|
|
9
7
|
return Buffer.from(value, "utf16le").toString("base64");
|
|
10
8
|
}
|
|
@@ -144,11 +142,12 @@ async function discoverWindows(input, identity) {
|
|
|
144
142
|
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
145
143
|
"$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
|
|
146
144
|
"if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
|
|
147
|
-
"$task=$tasks[0];$actions=@($task.Actions);",
|
|
148
|
-
|
|
149
|
-
`[pscustomobject]@{count=1;state=[int]$task.State;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};${windowsTaskPrincipalFields}}|ConvertTo-Json -Compress;`,
|
|
145
|
+
"$task=$tasks[0];$actions=@($task.Actions);$me=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
|
|
146
|
+
"[pscustomobject]@{count=1;state=[int]$task.State;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};principalMatches=([String]::Equals($task.Principal.UserId,$me,[StringComparison]::OrdinalIgnoreCase)-and [int]$task.Principal.LogonType -eq 3 -and [int]$task.Principal.RunLevel -eq 0)}|ConvertTo-Json -Compress;",
|
|
150
147
|
].join("");
|
|
151
|
-
const result = await input.runner("powershell.exe",
|
|
148
|
+
const result = await input.runner("powershell.exe", [
|
|
149
|
+
"-NoProfile", "-NonInteractive", "-Command", script, encoded(identity.id),
|
|
150
|
+
]);
|
|
152
151
|
if (result.exitCode !== 0) {
|
|
153
152
|
throw new Error(`Unable to inspect legacy scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
154
153
|
}
|
|
@@ -166,7 +165,7 @@ async function discoverWindows(input, identity) {
|
|
|
166
165
|
: null;
|
|
167
166
|
if (definition.count !== 1
|
|
168
167
|
|| definition.actionCount !== 1
|
|
169
|
-
||
|
|
168
|
+
|| definition.principalMatches !== true
|
|
170
169
|
|| !win32.isAbsolute(definition.execute ?? "")
|
|
171
170
|
|| match === null
|
|
172
171
|
|| match[2] !== input.desired.profile
|
package/dist/computer-main.js
CHANGED
|
File without changes
|
package/dist/computer-service.js
CHANGED
|
@@ -6,9 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { dirname, resolve } from "node:path";
|
|
7
7
|
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
8
8
|
import { daemonRegistryCandidates } from "./daemon-registry.js";
|
|
9
|
-
import { windowsTaskPrincipalFields, windowsTaskPrincipalMatches, windowsTaskPrincipalScript, } from "./windows-task-principal.js";
|
|
10
9
|
import { npmInvocation } from "./npm-command.js";
|
|
11
|
-
import { powershellCommandArgs } from "./powershell-command.js";
|
|
12
10
|
const execFileAsync = promisify(execFile);
|
|
13
11
|
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
14
12
|
const WINDOWS_STOP_POLL_MS = 100;
|
|
@@ -197,11 +195,12 @@ async function windowsTaskDefinitionMatches(spec, runner) {
|
|
|
197
195
|
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
198
196
|
"$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
|
|
199
197
|
"if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
|
|
200
|
-
"$task=$tasks[0];$actions=@($task.Actions);",
|
|
201
|
-
|
|
202
|
-
`[pscustomobject]@{count=1;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};${windowsTaskPrincipalFields}}|ConvertTo-Json -Compress;`,
|
|
198
|
+
"$task=$tasks[0];$actions=@($task.Actions);$me=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
|
|
199
|
+
"[pscustomobject]@{count=1;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};principalMatches=([String]::Equals($task.Principal.UserId,$me,[StringComparison]::OrdinalIgnoreCase)-and [int]$task.Principal.LogonType -eq 3 -and [int]$task.Principal.RunLevel -eq 0)}|ConvertTo-Json -Compress;",
|
|
203
200
|
].join("");
|
|
204
|
-
const result = await runner("powershell.exe",
|
|
201
|
+
const result = await runner("powershell.exe", [
|
|
202
|
+
"-NoProfile", "-NonInteractive", "-Command", script, encodedName,
|
|
203
|
+
]);
|
|
205
204
|
if (result.exitCode !== 0) {
|
|
206
205
|
throw new Error(`Unable to inspect scheduled task definition: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
207
206
|
}
|
|
@@ -215,7 +214,7 @@ async function windowsTaskDefinitionMatches(spec, runner) {
|
|
|
215
214
|
const expectedArguments = spec.daemonCommand.slice(1).map(windowsCommandArg).join(" ");
|
|
216
215
|
return definition.count === 1
|
|
217
216
|
&& definition.actionCount === 1
|
|
218
|
-
&&
|
|
217
|
+
&& definition.principalMatches === true
|
|
219
218
|
&& sameWindowsPath(definition.execute, spec.daemonCommand[0])
|
|
220
219
|
&& definition.arguments === expectedArguments;
|
|
221
220
|
}
|
|
@@ -227,7 +226,9 @@ async function windowsTaskState(spec, runner) {
|
|
|
227
226
|
"$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
|
|
228
227
|
"if($tasks.Count -eq 0){'missing'}elseif($tasks.Count -eq 1){'state:'+([int]$tasks[0].State)}else{throw 'duplicate scheduled tasks'}",
|
|
229
228
|
].join("");
|
|
230
|
-
const result = await runner("powershell.exe",
|
|
229
|
+
const result = await runner("powershell.exe", [
|
|
230
|
+
"-NoProfile", "-NonInteractive", "-Command", script, encodedName,
|
|
231
|
+
]);
|
|
231
232
|
if (result.exitCode !== 0) {
|
|
232
233
|
throw new Error(`Unable to query scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
233
234
|
}
|
|
@@ -278,9 +279,10 @@ export async function installService(spec, runner = systemCommandRunner) {
|
|
|
278
279
|
"$principal=New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited;",
|
|
279
280
|
"Register-ScheduledTask -TaskName $name -TaskPath '\\' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -ErrorAction Stop|Out-Null;",
|
|
280
281
|
].join("");
|
|
281
|
-
await checked(runner, "powershell.exe",
|
|
282
|
+
await checked(runner, "powershell.exe", [
|
|
283
|
+
"-NoProfile", "-NonInteractive", "-Command", script,
|
|
282
284
|
encodedName, encodedExecutable, encodedArguments,
|
|
283
|
-
]
|
|
285
|
+
], "Scheduled task installation");
|
|
284
286
|
return;
|
|
285
287
|
}
|
|
286
288
|
if (spec.descriptorPath === null || spec.descriptor === null)
|
|
@@ -416,33 +418,6 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
|
|
|
416
418
|
}
|
|
417
419
|
}
|
|
418
420
|
const systemDetachedSpawn = (command, args, options) => spawn(command, [...args], options);
|
|
419
|
-
function windowsRestartCommandArgs(spec) {
|
|
420
|
-
if (!spec.runtimeProcessIdentity)
|
|
421
|
-
throw new Error("Windows daemon process identity is required for restart");
|
|
422
|
-
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
423
|
-
const encodedExecutable = Buffer.from(spec.daemonCommand[0], "utf16le").toString("base64");
|
|
424
|
-
const encodedArguments = Buffer.from(spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "), "utf16le").toString("base64");
|
|
425
|
-
const script = [
|
|
426
|
-
"$ErrorActionPreference='Stop';",
|
|
427
|
-
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
428
|
-
"$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
|
|
429
|
-
"$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
|
|
430
|
-
"$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
|
|
431
|
-
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
432
|
-
"$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
|
|
433
|
-
"if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
|
|
434
|
-
"$p.Kill();",
|
|
435
|
-
"if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
|
|
436
|
-
"$deadline=[DateTime]::UtcNow.AddSeconds(10);",
|
|
437
|
-
"do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
|
|
438
|
-
"if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
|
|
439
|
-
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
440
|
-
"& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
|
|
441
|
-
].join("");
|
|
442
|
-
return powershellCommandArgs(script, [
|
|
443
|
-
String(process.pid), encodedName, encodedExecutable, encodedArguments, spec.runtimeProcessIdentity,
|
|
444
|
-
]);
|
|
445
|
-
}
|
|
446
421
|
const RESTART_HELPER_SOURCE = String.raw `
|
|
447
422
|
const { spawnSync } = require("node:child_process");
|
|
448
423
|
const spec = JSON.parse(process.argv[1]);
|
|
@@ -453,7 +428,27 @@ setTimeout(() => {
|
|
|
453
428
|
} else if (spec.platform === "linux") {
|
|
454
429
|
restarted = spawnSync("systemctl", ["--user", "--no-block", "restart", spec.id], { stdio: "ignore", shell: false });
|
|
455
430
|
} else {
|
|
456
|
-
|
|
431
|
+
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
432
|
+
const encodedExecutable = Buffer.from(spec.executable, "utf16le").toString("base64");
|
|
433
|
+
const encodedArguments = Buffer.from(spec.arguments, "utf16le").toString("base64");
|
|
434
|
+
const script = [
|
|
435
|
+
"$ErrorActionPreference='Stop';",
|
|
436
|
+
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
437
|
+
"$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
|
|
438
|
+
"$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
|
|
439
|
+
"$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
|
|
440
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
441
|
+
"$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
|
|
442
|
+
"if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
|
|
443
|
+
"$p.Kill();",
|
|
444
|
+
"if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
|
|
445
|
+
"$deadline=[DateTime]::UtcNow.AddSeconds(10);",
|
|
446
|
+
"do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
|
|
447
|
+
"if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
|
|
448
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
449
|
+
"& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
|
|
450
|
+
].join("");
|
|
451
|
+
restarted = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, String(spec.daemonPid), encodedName, encodedExecutable, encodedArguments, spec.processIdentity], { stdio: "ignore", shell: false });
|
|
457
452
|
}
|
|
458
453
|
if (restarted.status !== 0) process.send?.({ type: "restart-failed", status: restarted.status });
|
|
459
454
|
setTimeout(() => {
|
|
@@ -475,8 +470,15 @@ export function scheduleServiceRestart(spec, spawnDetached = systemDetachedSpawn
|
|
|
475
470
|
? { platform: spec.platform, id: spec.id }
|
|
476
471
|
: {
|
|
477
472
|
platform: spec.platform,
|
|
478
|
-
|
|
473
|
+
id: spec.id,
|
|
474
|
+
daemonPid: process.pid,
|
|
475
|
+
executable: spec.daemonCommand[0],
|
|
476
|
+
arguments: spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "),
|
|
477
|
+
processIdentity: spec.runtimeProcessIdentity,
|
|
479
478
|
};
|
|
479
|
+
if (spec.platform === "win32" && !payload.processIdentity) {
|
|
480
|
+
throw new Error("Windows daemon process identity is required for restart");
|
|
481
|
+
}
|
|
480
482
|
const child = spawnDetached(process.execPath, ["-e", RESTART_HELPER_SOURCE, JSON.stringify(payload)], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"], shell: false });
|
|
481
483
|
child.unref();
|
|
482
484
|
let settleFailure;
|
|
@@ -625,7 +627,7 @@ export async function windowsProfileIsPrivate(path, runner = systemCommandRunner
|
|
|
625
627
|
"$unsafe=$rules|Where-Object { $_.AccessControlType -eq 'Allow' -and $safe -notcontains $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value };",
|
|
626
628
|
"if($unsafe){'false'}else{'true'}",
|
|
627
629
|
].join("");
|
|
628
|
-
const result = await runner("powershell.exe",
|
|
630
|
+
const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, encodedPath]);
|
|
629
631
|
if (result.exitCode !== 0)
|
|
630
632
|
throw new Error(`Profile ACL inspection failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
631
633
|
if (result.stdout.trim() === "true")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import { MachineTokenSchema } from "./computer-profile.js";
|
|
4
|
-
import { powershellCommandArgs } from "./powershell-command.js";
|
|
5
4
|
import { systemCommandRunner, } from "./computer-service.js";
|
|
6
5
|
const HttpUrlSchema = z.string().url().refine((value) => {
|
|
7
6
|
const url = new URL(value);
|
|
@@ -39,19 +38,6 @@ const SetupBootstrapSchema = z.object({
|
|
|
39
38
|
expiresAt: z.string().datetime({ offset: true }).optional(),
|
|
40
39
|
machineToken: MachineTokenSchema,
|
|
41
40
|
}).strict();
|
|
42
|
-
const SetupTokenTargetSchema = SetupBootstrapSchema.omit({ setupId: true, machineToken: true }).extend({
|
|
43
|
-
workspaceSlug: z.string().min(1).max(80),
|
|
44
|
-
releaseTag: z.enum(["latest", "beta"]),
|
|
45
|
-
connection: z.object({
|
|
46
|
-
status: z.enum(["online", "offline"]),
|
|
47
|
-
epoch: z.number().int().nonnegative(),
|
|
48
|
-
profile: z.string().nullable(),
|
|
49
|
-
daemonHome: z.string().nullable(),
|
|
50
|
-
agentsRoot: z.string().nullable(),
|
|
51
|
-
storageId: z.string().optional(),
|
|
52
|
-
credentialMatchesConnection: z.boolean(),
|
|
53
|
-
}).strict(),
|
|
54
|
-
}).strict();
|
|
55
41
|
const SetupPendingSchema = z.object({
|
|
56
42
|
status: z.enum(["authorization_pending", "pending"]),
|
|
57
43
|
intervalSeconds: z.number().int().positive().max(300).optional(),
|
|
@@ -250,8 +236,8 @@ export function createComputerSetupClient(options) {
|
|
|
250
236
|
setTimeout(resolve, milliseconds);
|
|
251
237
|
}));
|
|
252
238
|
const now = options.now ?? Date.now;
|
|
239
|
+
let activeMachineId;
|
|
253
240
|
const request = async (method, path, body, bearer) => {
|
|
254
|
-
const secretInputs = [bearer ?? "", body?.deviceCode ?? ""];
|
|
255
241
|
const controller = new AbortController();
|
|
256
242
|
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
257
243
|
try {
|
|
@@ -266,7 +252,7 @@ export function createComputerSetupClient(options) {
|
|
|
266
252
|
});
|
|
267
253
|
const payload = await response.json();
|
|
268
254
|
if (!response.ok || payload?.success === false) {
|
|
269
|
-
const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})
|
|
255
|
+
const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
|
|
270
256
|
throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
|
|
271
257
|
}
|
|
272
258
|
return (payload?.success === true ? payload.data : payload);
|
|
@@ -276,15 +262,15 @@ export function createComputerSetupClient(options) {
|
|
|
276
262
|
throw new Error("computer setup request timed out");
|
|
277
263
|
if (error instanceof ComputerSetupHttpError)
|
|
278
264
|
throw error;
|
|
279
|
-
throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)
|
|
265
|
+
throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
|
|
280
266
|
}
|
|
281
267
|
finally {
|
|
282
268
|
clearTimeout(timeoutId);
|
|
283
269
|
}
|
|
284
270
|
};
|
|
285
271
|
return {
|
|
286
|
-
tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
|
|
287
272
|
start: async (input) => {
|
|
273
|
+
activeMachineId = input.machineId;
|
|
288
274
|
return parseStart(await request("POST", "/api/auth/computer-setup/start", {
|
|
289
275
|
...(input.workspace.kind === "id"
|
|
290
276
|
? { workspaceId: input.workspace.value }
|
|
@@ -295,6 +281,8 @@ export function createComputerSetupClient(options) {
|
|
|
295
281
|
}));
|
|
296
282
|
},
|
|
297
283
|
poll: async (deviceCode) => {
|
|
284
|
+
if (activeMachineId === undefined)
|
|
285
|
+
throw new Error("computer setup polling has no active machine");
|
|
298
286
|
return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
|
|
299
287
|
},
|
|
300
288
|
waitForHello: async (input) => {
|
|
@@ -321,10 +309,8 @@ export function createComputerSetupClient(options) {
|
|
|
321
309
|
};
|
|
322
310
|
}
|
|
323
311
|
export async function openSetupBrowser(url, platform = process.platform, runner = systemCommandRunner) {
|
|
324
|
-
const command = platform === "win32" ? "
|
|
325
|
-
const args =
|
|
326
|
-
? powershellCommandArgs("Start-Process -FilePath $args[0] -ErrorAction Stop | Out-Null", [url])
|
|
327
|
-
: [url];
|
|
312
|
+
const command = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
|
|
313
|
+
const args = [url];
|
|
328
314
|
const result = await runner(command, args);
|
|
329
315
|
if (result.exitCode !== 0) {
|
|
330
316
|
throw new Error(`Unable to open the computer setup browser: ${redactSetupSecrets(result.stderr.trim() || `exit ${result.exitCode}`)}`);
|
|
@@ -6,7 +6,7 @@ function safeError(error, machineToken) {
|
|
|
6
6
|
const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
|
|
7
7
|
return new Error(message, error instanceof Error ? { cause: error } : undefined);
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
function assertBootstrapMatches(bootstrap, input) {
|
|
10
10
|
if (bootstrap.machineId !== input.args.machineId) {
|
|
11
11
|
throw new Error("setup bootstrap machine does not match --machine");
|
|
12
12
|
}
|
|
@@ -40,7 +40,7 @@ function secondsUntil(expiresAt, now) {
|
|
|
40
40
|
const remaining = Date.parse(expiresAt) - now();
|
|
41
41
|
return Math.max(0, Math.ceil(remaining / 1_000));
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
function localPathsMatch(left, right, platform) {
|
|
44
44
|
const paths = platform === "win32" ? win32 : posix;
|
|
45
45
|
if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
|
|
46
46
|
return false;
|
|
@@ -51,14 +51,6 @@ export function localPathsMatch(left, right, platform) {
|
|
|
51
51
|
return normalize(left) === normalize(right);
|
|
52
52
|
}
|
|
53
53
|
async function obtainBootstrap(input, deps) {
|
|
54
|
-
if (input.args.setupToken !== undefined) {
|
|
55
|
-
deps.reportPairing("Checking computer connection information...");
|
|
56
|
-
const result = await deps.client.poll(input.args.setupToken);
|
|
57
|
-
if (result.status !== "authorized") {
|
|
58
|
-
throw new Error("Setup command expired or unavailable; regenerate it in NowWork and run the new command");
|
|
59
|
-
}
|
|
60
|
-
return result.bootstrap;
|
|
61
|
-
}
|
|
62
54
|
const pairing = await deps.client.start({
|
|
63
55
|
workspace: input.args.workspace,
|
|
64
56
|
machineId: input.args.machineId,
|
|
@@ -124,7 +116,6 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
124
116
|
bootstrap = authorized;
|
|
125
117
|
machineToken = token;
|
|
126
118
|
assertBootstrapMatches(authorized, input);
|
|
127
|
-
deps.reportPairing("Downloading, installing, and starting the Daemon service...");
|
|
128
119
|
const installation = await deps.installAutomatically({
|
|
129
120
|
serverUrl: authorized.serverUrl,
|
|
130
121
|
machineToken: token,
|
|
@@ -137,7 +128,6 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
137
128
|
...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
|
|
138
129
|
beforeRollback: confirmRollback,
|
|
139
130
|
verifyAfterStart: async (candidate) => {
|
|
140
|
-
deps.reportPairing("Confirming the computer connection...");
|
|
141
131
|
const hello = await deps.client.waitForHello({
|
|
142
132
|
machineToken: token,
|
|
143
133
|
machineId: authorized.machineId,
|
|
@@ -184,7 +174,6 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
184
174
|
});
|
|
185
175
|
return {
|
|
186
176
|
workspaceId: authorized.workspaceId,
|
|
187
|
-
...(authorized.workspaceSlug === undefined ? {} : { workspaceSlug: authorized.workspaceSlug }),
|
|
188
177
|
machineId: authorized.machineId,
|
|
189
178
|
environment: authorized.environment,
|
|
190
179
|
profile: installation.profile,
|
package/dist/computer-setup.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { DEFAULT_SERVER_URL } from "./config.js";
|
|
4
|
-
import { MachineTokenSchema } from "./computer-profile.js";
|
|
5
4
|
import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
|
|
6
5
|
import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
7
6
|
import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
|
|
8
7
|
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
9
8
|
import { systemCommandRunner } from "./computer-service.js";
|
|
10
|
-
import { runMachineTokenSetup } from "./computer-token-setup.js";
|
|
11
9
|
const WorkspaceSlugSchema = z.string()
|
|
12
10
|
.min(2)
|
|
13
11
|
.max(40)
|
|
@@ -72,8 +70,6 @@ export function parseSetupArgs(argv) {
|
|
|
72
70
|
assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
73
71
|
assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
74
72
|
assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
|
|
75
|
-
assertRepeatedOptionValuesAgree(argv, "setup-token", (value) => value);
|
|
76
|
-
assertRepeatedOptionValuesAgree(argv, "api-key", (value) => value);
|
|
77
73
|
const parsed = parseArgs({
|
|
78
74
|
args: [...argv],
|
|
79
75
|
allowPositionals: true,
|
|
@@ -86,8 +82,6 @@ export function parseSetupArgs(argv) {
|
|
|
86
82
|
"server-url": { type: "string" },
|
|
87
83
|
json: { type: "boolean", default: false },
|
|
88
84
|
"no-browser": { type: "boolean", default: false },
|
|
89
|
-
"setup-token": { type: "string" },
|
|
90
|
-
"api-key": { type: "string" },
|
|
91
85
|
help: { type: "boolean", short: "h", default: false },
|
|
92
86
|
},
|
|
93
87
|
});
|
|
@@ -130,14 +124,6 @@ export function parseSetupArgs(argv) {
|
|
|
130
124
|
}
|
|
131
125
|
const machineId = parsedMachine;
|
|
132
126
|
const serverUrl = stringValue(values, "server-url");
|
|
133
|
-
const setupToken = stringValue(values, "setup-token");
|
|
134
|
-
const machineToken = stringValue(values, "api-key");
|
|
135
|
-
if (setupToken !== undefined && machineToken !== undefined)
|
|
136
|
-
throw new Error("Choose one setup credential source");
|
|
137
|
-
if (setupToken !== undefined && !/^[A-Za-z0-9_-]{32,512}$/u.test(setupToken))
|
|
138
|
-
throw new Error("Invalid setup Token");
|
|
139
|
-
if (machineToken !== undefined)
|
|
140
|
-
MachineTokenSchema.parse(machineToken);
|
|
141
127
|
return Object.freeze({
|
|
142
128
|
command: "setup",
|
|
143
129
|
workspace,
|
|
@@ -145,8 +131,6 @@ export function parseSetupArgs(argv) {
|
|
|
145
131
|
...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
|
|
146
132
|
...(booleanValue(values, "json") ? { json: true } : {}),
|
|
147
133
|
...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
|
|
148
|
-
...(setupToken === undefined ? {} : { setupToken }),
|
|
149
|
-
...(machineToken === undefined ? {} : { machineToken }),
|
|
150
134
|
});
|
|
151
135
|
}
|
|
152
136
|
function resolveSetupServerUrl(args, env) {
|
|
@@ -164,8 +148,7 @@ export function setupUsage(td = (message) => message) {
|
|
|
164
148
|
" nowwork-computer setup --workspace /workspace --machine <uuid>",
|
|
165
149
|
" nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
|
|
166
150
|
"",
|
|
167
|
-
"Options: --server-url <url> --
|
|
168
|
-
td("Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported."),
|
|
151
|
+
"Options: --server-url <url> --json --no-browser",
|
|
169
152
|
].join("\n") + "\n";
|
|
170
153
|
}
|
|
171
154
|
function defaults() {
|
|
@@ -184,7 +167,6 @@ function defaults() {
|
|
|
184
167
|
}
|
|
185
168
|
function humanSummary(result, td) {
|
|
186
169
|
return [
|
|
187
|
-
td("Connected to {{workspace}}.", { workspace: result.workspaceSlug ?? result.workspaceId }),
|
|
188
170
|
td("Workspace: {{value}}", { value: result.workspaceId }),
|
|
189
171
|
td("Machine: {{value}}", { value: result.machineId }),
|
|
190
172
|
td("Environment: {{value}}", { value: result.environment }),
|
|
@@ -223,7 +205,6 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
|
|
|
223
205
|
return null;
|
|
224
206
|
const deps = { ...defaults(), ...overrides };
|
|
225
207
|
const lang = detectDaemonLang(deps.env);
|
|
226
|
-
const secrets = [...optionOccurrences(argv, "setup-token"), ...optionOccurrences(argv, "api-key"), deps.env.CREW_MACHINE_TOKEN ?? ""];
|
|
227
208
|
try {
|
|
228
209
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
229
210
|
deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
|
|
@@ -232,43 +213,32 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
|
|
|
232
213
|
let args;
|
|
233
214
|
try {
|
|
234
215
|
args = parseSetupArgs(argv);
|
|
235
|
-
const envToken = deps.env.CREW_MACHINE_TOKEN?.trim();
|
|
236
|
-
if (envToken) {
|
|
237
|
-
if (args.setupToken !== undefined || (args.machineToken !== undefined && args.machineToken !== envToken)) {
|
|
238
|
-
throw new Error("Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token");
|
|
239
|
-
}
|
|
240
|
-
args = Object.freeze({ ...args, machineToken: MachineTokenSchema.parse(envToken) });
|
|
241
|
-
}
|
|
242
216
|
}
|
|
243
217
|
catch (error) {
|
|
244
|
-
deps.stderr(`nowwork-computer: ${
|
|
218
|
+
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
245
219
|
return 2;
|
|
246
220
|
}
|
|
247
|
-
if (args.json === true
|
|
221
|
+
if (args.json === true) {
|
|
248
222
|
deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
|
|
249
223
|
}
|
|
250
224
|
const serverUrl = resolveSetupServerUrl(args, deps.env);
|
|
251
225
|
const createClient = overrides.createClient
|
|
252
226
|
?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
|
|
253
|
-
const
|
|
227
|
+
const result = await runComputerSetupOrchestration({ args, serverUrl }, {
|
|
254
228
|
client: createClient(serverUrl),
|
|
255
229
|
installAutomatically: deps.installAutomatically,
|
|
256
230
|
openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
|
|
257
231
|
sleep: deps.sleep,
|
|
258
232
|
reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
|
|
259
233
|
now: deps.now,
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
const result = args.machineToken === undefined
|
|
263
|
-
? await runComputerSetupOrchestration({ args, serverUrl }, orchestrationDeps)
|
|
264
|
-
: await runMachineTokenSetup({ args, serverUrl }, orchestrationDeps, args.machineToken);
|
|
234
|
+
});
|
|
265
235
|
deps.stdout(args.json === true
|
|
266
236
|
? `${JSON.stringify(result)}\n`
|
|
267
237
|
: humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
|
|
268
238
|
return 0;
|
|
269
239
|
}
|
|
270
240
|
catch (error) {
|
|
271
|
-
deps.stderr(`nowwork-computer: ${
|
|
241
|
+
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
272
242
|
return 1;
|
|
273
243
|
}
|
|
274
244
|
}
|
package/dist/execution-runner.js
CHANGED
|
@@ -587,6 +587,26 @@ export async function runExecution(config, input, dependencies) {
|
|
|
587
587
|
let activitySequence = 0;
|
|
588
588
|
let consoleSequence = 0;
|
|
589
589
|
let externalOutputSequence = 0;
|
|
590
|
+
const bufferedPiExternalOutput = [];
|
|
591
|
+
const reportExternalOutput = (text) => {
|
|
592
|
+
if (firstOutputTimer !== undefined) {
|
|
593
|
+
clearTimeout(firstOutputTimer);
|
|
594
|
+
firstOutputTimer = undefined;
|
|
595
|
+
}
|
|
596
|
+
const frame = DaemonToServerExecutionFrameSchema.parse({
|
|
597
|
+
type: "execution:output",
|
|
598
|
+
protocolVersion: 1,
|
|
599
|
+
executionId: spec.executionId,
|
|
600
|
+
channel: "external_answer",
|
|
601
|
+
text,
|
|
602
|
+
seq: externalOutputSequence++,
|
|
603
|
+
at: now().toISOString(),
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
|
|
607
|
+
}
|
|
608
|
+
catch { /* final completion remains the authoritative repair */ }
|
|
609
|
+
};
|
|
590
610
|
const callbacks = {
|
|
591
611
|
onRuntimeStarting: () => {
|
|
592
612
|
dependencies.onRuntimePhase?.("starting", spec.runtime.name);
|
|
@@ -711,23 +731,11 @@ export async function runExecution(config, input, dependencies) {
|
|
|
711
731
|
} : {}),
|
|
712
732
|
...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
|
|
713
733
|
onExternalOutput: (text) => {
|
|
714
|
-
if (
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
const frame = DaemonToServerExecutionFrameSchema.parse({
|
|
719
|
-
type: "execution:output",
|
|
720
|
-
protocolVersion: 1,
|
|
721
|
-
executionId: spec.executionId,
|
|
722
|
-
channel: "external_answer",
|
|
723
|
-
text,
|
|
724
|
-
seq: externalOutputSequence++,
|
|
725
|
-
at: now().toISOString(),
|
|
726
|
-
});
|
|
727
|
-
try {
|
|
728
|
-
telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
|
|
734
|
+
if (spec.runtime.name === "pi") {
|
|
735
|
+
bufferedPiExternalOutput.push(text);
|
|
736
|
+
return;
|
|
729
737
|
}
|
|
730
|
-
|
|
738
|
+
reportExternalOutput(text);
|
|
731
739
|
},
|
|
732
740
|
} : {}),
|
|
733
741
|
};
|
|
@@ -909,6 +917,10 @@ export async function runExecution(config, input, dependencies) {
|
|
|
909
917
|
clearTimeout(timeout);
|
|
910
918
|
if (firstOutputTimer !== undefined)
|
|
911
919
|
clearTimeout(firstOutputTimer);
|
|
920
|
+
if (spec.runtime.name === "pi" && result.exitCode === 0 && result.externalAnswer !== null) {
|
|
921
|
+
for (const text of bufferedPiExternalOutput)
|
|
922
|
+
reportExternalOutput(text);
|
|
923
|
+
}
|
|
912
924
|
const finishedAt = now().toISOString();
|
|
913
925
|
if (result.exitCode === 0 && result.finalText?.trim())
|
|
914
926
|
memoryCaptureFinalText = result.finalText;
|
package/dist/external-output.js
CHANGED
|
@@ -110,6 +110,7 @@ export class ExternalAnswerDecoder {
|
|
|
110
110
|
pending = "";
|
|
111
111
|
block = "";
|
|
112
112
|
closed = null;
|
|
113
|
+
rejected = false;
|
|
113
114
|
push(text) {
|
|
114
115
|
if (!text)
|
|
115
116
|
return [];
|
|
@@ -125,7 +126,7 @@ export class ExternalAnswerDecoder {
|
|
|
125
126
|
}
|
|
126
127
|
/** 整轮里最后一个完整闭合的标签块(已 trim);没有闭合过任何块则为 null。 */
|
|
127
128
|
get lastClosedBlock() {
|
|
128
|
-
return this.closed;
|
|
129
|
+
return this.rejected ? null : this.closed;
|
|
129
130
|
}
|
|
130
131
|
consume(value, output) {
|
|
131
132
|
this.pending = "";
|
|
@@ -154,12 +155,21 @@ export class ExternalAnswerDecoder {
|
|
|
154
155
|
this.block = "";
|
|
155
156
|
this.state = "inside";
|
|
156
157
|
}
|
|
158
|
+
else if (!tag.closing && this.state === "inside") {
|
|
159
|
+
// 嵌套答案标记说明模型正在讨论协议或混入了思考文本;
|
|
160
|
+
// 不要把内层短片段误当成最终答案。
|
|
161
|
+
this.rejected = true;
|
|
162
|
+
this.block = "";
|
|
163
|
+
this.state = "outside";
|
|
164
|
+
}
|
|
157
165
|
else {
|
|
166
|
+
this.rejected = true;
|
|
158
167
|
this.block = "";
|
|
159
168
|
this.state = tag.closing ? "outside" : "inside";
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
else if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
172
|
+
this.rejected = true;
|
|
163
173
|
this.block = "";
|
|
164
174
|
this.state = "outside";
|
|
165
175
|
}
|
|
@@ -171,6 +181,7 @@ export class ExternalAnswerDecoder {
|
|
|
171
181
|
else if (this.pending.length > MAX_MARKER_TAG_LENGTH + 2) {
|
|
172
182
|
const tag = classifyMarkerTag(this.pending.slice(1));
|
|
173
183
|
if (tag.kind === "suspicious" || tag.kind === "known") {
|
|
184
|
+
this.rejected = true;
|
|
174
185
|
this.block = "";
|
|
175
186
|
this.state = "outside";
|
|
176
187
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -36,16 +36,6 @@ const zh = {
|
|
|
36
36
|
"Code {{code}}": "验证码 {{code}}",
|
|
37
37
|
"Browser open skipped: {{reason}}": "未能自动打开浏览器:{{reason}}",
|
|
38
38
|
"Starting computer setup pairing...": "正在发起电脑接入授权…",
|
|
39
|
-
"Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported.": "Token 接入无需浏览器授权,也支持 CREW_MACHINE_TOKEN。",
|
|
40
|
-
"Checking computer connection information...": "正在检查接入信息…",
|
|
41
|
-
"Downloading, installing, and starting the Daemon service...": "正在下载、安装并启动 Daemon 服务…",
|
|
42
|
-
"Confirming the computer connection...": "正在确认电脑连接…",
|
|
43
|
-
"Connected to {{workspace}}.": "已接入 {{workspace}}。",
|
|
44
|
-
"Setup command expired or unavailable; regenerate it in NowWork and run the new command": "接入命令已过期或不可用,请在 NowWork 重新生成并执行新命令。",
|
|
45
|
-
"Timed out confirming the new computer connection": "确认新连接超时,请检查网络和服务状态。",
|
|
46
|
-
"Invalid setup Token": "接入 Token 无效。",
|
|
47
|
-
"Choose one setup credential source": "请只使用一种接入凭据。",
|
|
48
|
-
"Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token": "请只使用一种接入凭据;使用其他 Token 时请清除 CREW_MACHINE_TOKEN。",
|
|
49
39
|
"Workspace: {{value}}": "工作区:{{value}}",
|
|
50
40
|
"Machine: {{value}}": "机器: {{value}}",
|
|
51
41
|
"Environment: {{value}}": "环境:{{value}}",
|