@nowcrew/daemon 0.5.14 → 0.5.16

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 CHANGED
@@ -15,6 +15,57 @@ The process stays resident and reconnects with backoff. For local development, r
15
15
  `pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
16
16
  manual one-shot entry.
17
17
 
18
+ ## Computer Profiles And Service Lifecycle
19
+
20
+ Persist a named connection without putting its machine token in shell history or service arguments:
21
+
22
+ ```bash
23
+ printf '%s' "$CREW_MACHINE_TOKEN" | crew-daemon profile save work \
24
+ --server-url https://nowwork.example --token-stdin
25
+ crew-daemon profile list
26
+ crew-daemon profile show work # reports configured=true; never prints the token
27
+ crew-daemon serve --profile work
28
+ ```
29
+
30
+ Profile files live under `~/.crew/daemon/profiles`. Unix writes use mode `0600`; Windows tokens are
31
+ encrypted with CurrentUser DPAPI and the profile ACL is restricted to the current user. Windows refuses
32
+ legacy plaintext profiles and fails the save if DPAPI or ACL hardening is unavailable.
33
+
34
+ Install and operate the profile as a user service:
35
+
36
+ ```bash
37
+ npm install --global @nowcrew/daemon@latest
38
+ crew-daemon install --profile work
39
+ crew-daemon start --profile work
40
+ crew-daemon status --profile work
41
+ crew-daemon doctor --profile work
42
+ crew-daemon restart --profile work
43
+ crew-daemon stop --profile work
44
+ crew-daemon uninstall --profile work
45
+ crew-daemon profile remove work
46
+ ```
47
+
48
+ Lifecycle installation is intentionally refused from an `npx`/temporary cache entry because the service
49
+ must keep a stable executable path across cache cleanup and upgrades.
50
+
51
+ macOS uses a user LaunchAgent, Linux uses a systemd user unit, and Windows uses Task Scheduler at logon.
52
+ On macOS `install` writes the plist, `start` bootstraps it, and `stop` boots it out, so `KeepAlive` cannot
53
+ silently resurrect a stopped daemon. Always use `status` as the source of truth instead of assuming that
54
+ an install or start request proves the process is running.
55
+ Profiles also capture the install-time runtime `PATH`, so user-installed CLIs remain discoverable under
56
+ minimal service-manager environments. Service descriptions contain only `serve --profile work` and the
57
+ non-secret profile directory; the token and PATH are read by the daemon at runtime and never appear in service arguments. Lifecycle commands fail
58
+ when the native manager rejects an operation. Windows status uses the
59
+ locale-independent `Get-ScheduledTask.State` enum and reports healthy only for `Running`. Its scheduled
60
+ task has no execution time limit, runs on battery, and restarts after failures.
61
+
62
+ Upgrade the global daemon package, optionally restarting one installed profile afterward:
63
+
64
+ ```bash
65
+ crew-daemon upgrade
66
+ crew-daemon upgrade --profile work
67
+ ```
68
+
18
69
  ## Execution Boundary
19
70
 
20
71
  The server selects the machine and sends a validated `execution:start` containing:
@@ -29,9 +80,12 @@ the local workspace/environment, and launches only a built-in runtime adapter (`
29
80
  `kimi`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
30
81
  behavior. Those are server responsibilities.
31
82
 
32
- Protocol support and limits are advertised in `machine:hello`. Unknown required protocol semantics are
33
- rejected before side effects. Protocol-0 `agent:start` remains only for the server-governed compatibility
34
- window.
83
+ Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
84
+ found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
85
+ adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
86
+ Old daemons that omit `executionRuntimes` are conservatively interpreted as the intersection of installed
87
+ CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
88
+ Protocol-0 `agent:start` remains only for the server-governed compatibility window.
35
89
 
36
90
  ## Reliability
37
91
 
@@ -0,0 +1,22 @@
1
+ import { readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export function boundImDecisionFilePath(runDir) {
4
+ return join(runDir, ".bound-im-decision.json");
5
+ }
6
+ export async function resetBoundImDecisionFile(path) {
7
+ await unlink(path).catch((error) => {
8
+ if (error.code !== "ENOENT")
9
+ throw error;
10
+ });
11
+ }
12
+ export async function readBoundImDecisionFile(path) {
13
+ try {
14
+ const parsed = JSON.parse(await readFile(path, "utf8"));
15
+ if (!parsed || typeof parsed !== "object")
16
+ return null;
17
+ return parsed.decision === "notify" ? { decision: "notify" } : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
@@ -0,0 +1,214 @@
1
+ import { parseArgs } from "node:util";
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { daemonHome, listProfiles, loadProfile, profileIsPrivate, profilePath, publicProfile, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
6
+ import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
7
+ import { detectDaemonLang, formatDaemonText } from "./i18n.js";
8
+ const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
9
+ export function builtDaemonEntry(moduleUrl = import.meta.url) {
10
+ return moduleUrl.endsWith(".js")
11
+ ? fileURLToPath(new URL("./main.js", moduleUrl))
12
+ : "";
13
+ }
14
+ function defaults() {
15
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
16
+ return {
17
+ env: process.env,
18
+ platform: process.platform,
19
+ userHome: homedir(),
20
+ uid: process.getuid?.(),
21
+ nodePath: process.execPath,
22
+ entryPath: builtDaemonEntry(),
23
+ runner: systemCommandRunner,
24
+ ...(process.platform === "win32" ? { profileProtector: windowsDpapiProtector } : {}),
25
+ resolveGlobalNodeModules: async () => {
26
+ const result = await systemCommandRunner(npmCommand, ["root", "--global"]);
27
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
28
+ throw new Error(`Unable to locate global npm modules: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
29
+ }
30
+ return result.stdout.trim();
31
+ },
32
+ readStdin: async () => {
33
+ if (process.stdin.isTTY)
34
+ throw new Error("--token-stdin requires a token on standard input");
35
+ const chunks = [];
36
+ for await (const chunk of process.stdin)
37
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
38
+ return Buffer.concat(chunks).toString("utf8");
39
+ },
40
+ stdout: (text) => process.stdout.write(text),
41
+ stderr: (text) => process.stderr.write(text),
42
+ };
43
+ }
44
+ export function isGlobalDaemonEntry(entryPath, globalNodeModules) {
45
+ return resolve(entryPath) === resolve(globalNodeModules, "@nowcrew", "daemon", "dist", "main.js");
46
+ }
47
+ function usage(td) {
48
+ return [
49
+ td("Computer lifecycle:"),
50
+ " crew-daemon profile save <name> --server-url <url> [--agents-root <path>] [--token-stdin]",
51
+ " crew-daemon profile list|show|remove [<name>]",
52
+ " crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
53
+ " crew-daemon upgrade [--profile <name>]",
54
+ "",
55
+ td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
56
+ ].join("\n") + "\n";
57
+ }
58
+ function requireValue(value, name, td) {
59
+ if (!value)
60
+ throw new Error(td("Missing {{name}}", { name }));
61
+ return value;
62
+ }
63
+ export async function runComputerCommand(argv, overrides = {}) {
64
+ const command = argv[0];
65
+ if (!command || !COMPUTER_COMMANDS.has(command))
66
+ return null;
67
+ const deps = { ...defaults(), ...overrides };
68
+ const lang = detectDaemonLang(deps.env);
69
+ const td = (message, values) => formatDaemonText(lang, message, values);
70
+ try {
71
+ const parsed = parseArgs({
72
+ args: [...argv],
73
+ allowPositionals: true,
74
+ strict: true,
75
+ options: {
76
+ profile: { type: "string" },
77
+ "server-url": { type: "string" },
78
+ "agents-root": { type: "string" },
79
+ "token-stdin": { type: "boolean", default: false },
80
+ help: { type: "boolean", short: "h", default: false },
81
+ },
82
+ });
83
+ if (parsed.values.help) {
84
+ deps.stdout(usage(td));
85
+ return 0;
86
+ }
87
+ const [, action, positionalName] = parsed.positionals;
88
+ const home = daemonHome(deps.env);
89
+ const profileStorage = {
90
+ platform: deps.platform,
91
+ ...(deps.profileProtector ? { protector: deps.profileProtector } : {}),
92
+ ...(deps.platform === "win32"
93
+ ? { harden: (path) => hardenWindowsProfile(path, deps.runner) }
94
+ : {}),
95
+ };
96
+ if (command === "profile") {
97
+ if (action === "list") {
98
+ deps.stdout(`${JSON.stringify(await listProfiles(home), null, 2)}\n`);
99
+ return 0;
100
+ }
101
+ const name = requireValue(positionalName, "profile name", td);
102
+ if (action === "show") {
103
+ deps.stdout(`${JSON.stringify(publicProfile(await loadProfile(name, home, profileStorage)), null, 2)}\n`);
104
+ return 0;
105
+ }
106
+ if (action === "remove") {
107
+ await loadProfile(name, home, profileStorage);
108
+ const spec = buildServiceSpec({
109
+ ...deps, profile: name, profileHome: home,
110
+ });
111
+ const status = await serviceStatus(spec, deps.runner);
112
+ if (status.installed)
113
+ throw new Error(td("Service '{{id}}' is still installed; uninstall it before removing the profile", { id: spec.id }));
114
+ await removeProfile(name, home);
115
+ deps.stdout(`${td("Removed profile '{{name}}'.", { name })}\n`);
116
+ return 0;
117
+ }
118
+ if (action !== "save")
119
+ throw new Error(td("Expected profile save, list, show, or remove"));
120
+ const fromStdin = parsed.values["token-stdin"];
121
+ if (fromStdin && deps.env.CREW_MACHINE_TOKEN) {
122
+ throw new Error(td("Choose one token source: CREW_MACHINE_TOKEN or --token-stdin"));
123
+ }
124
+ const machineToken = (fromStdin ? await deps.readStdin() : deps.env.CREW_MACHINE_TOKEN)?.trim();
125
+ if (!machineToken)
126
+ throw new Error(td("Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin"));
127
+ await saveProfile({
128
+ name,
129
+ serverUrl: requireValue(parsed.values["server-url"], "--server-url", td),
130
+ machineToken,
131
+ ...(parsed.values["agents-root"] ? { agentsRoot: resolve(parsed.values["agents-root"]) } : {}),
132
+ ...(deps.env.PATH ? { runtimePath: deps.env.PATH } : {}),
133
+ }, home, profileStorage);
134
+ deps.stdout(`${td("Saved profile '{{name}}' with private credentials.", { name })}\n`);
135
+ return 0;
136
+ }
137
+ const profileName = parsed.values.profile;
138
+ if (command === "upgrade") {
139
+ const spec = profileName
140
+ ? buildServiceSpec({
141
+ ...deps,
142
+ profile: (await loadProfile(profileName, home, profileStorage)).name,
143
+ profileHome: home,
144
+ })
145
+ : null;
146
+ if (spec) {
147
+ if (!isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
148
+ throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest"));
149
+ }
150
+ const before = await serviceStatus(spec, deps.runner);
151
+ if (!before.installed)
152
+ throw new Error(td("Service '{{id}}' is not installed", { id: spec.id }));
153
+ }
154
+ await upgradeDaemon(deps.platform, deps.runner);
155
+ if (profileName) {
156
+ await serviceAction(spec, "restart", deps.runner);
157
+ deps.stdout(`${td("Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.", { name: profileName })}\n`);
158
+ }
159
+ else {
160
+ deps.stdout(`${td("Upgraded daemon. Installed services were not restarted; pass --profile to restart one.")}\n`);
161
+ }
162
+ return 0;
163
+ }
164
+ const name = requireValue(profileName, "--profile", td);
165
+ await loadProfile(name, home, profileStorage);
166
+ if (!deps.entryPath.endsWith(".js")) {
167
+ throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
168
+ }
169
+ const spec = buildServiceSpec({
170
+ ...deps, profile: name, profileHome: home,
171
+ });
172
+ if (command === "install") {
173
+ if (!isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
174
+ throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest"));
175
+ }
176
+ await installService(spec, deps.runner);
177
+ deps.stdout(`${td("Installed '{{id}}'. Use status to confirm runtime state.", { id: spec.id })}\n`);
178
+ return 0;
179
+ }
180
+ if (command === "uninstall") {
181
+ await uninstallService(spec, deps.runner);
182
+ deps.stdout(`${td("Uninstalled '{{id}}'.", { id: spec.id })}\n`);
183
+ return 0;
184
+ }
185
+ if (command === "start" || command === "stop" || command === "restart") {
186
+ await serviceAction(spec, command, deps.runner);
187
+ deps.stdout(`${td("{{action}} request accepted for '{{id}}'. Verify with status.", {
188
+ action: command,
189
+ id: spec.id,
190
+ })}\n`);
191
+ return 0;
192
+ }
193
+ if (command === "status") {
194
+ const status = await serviceStatus(spec, deps.runner);
195
+ deps.stdout(`${JSON.stringify({ installed: status.installed, loaded: status.loaded, running: status.running, detail: (status.stdout || status.stderr).trim() }, null, 2)}\n`);
196
+ return status.installed && status.running ? 0 : 3;
197
+ }
198
+ if (command === "doctor") {
199
+ const privateFile = deps.platform === "win32"
200
+ ? await windowsProfileIsPrivate(profilePath(name, home), deps.runner)
201
+ : await profileIsPrivate(name, home, deps.platform);
202
+ const checks = await doctorService(spec, privateFile, deps.runner);
203
+ const registered = (await serviceStatus(spec, deps.runner)).installed;
204
+ deps.stdout(`${JSON.stringify({ checks, registered }, null, 2)}\n`);
205
+ return checks.every((check) => check.ok) ? 0 : 4;
206
+ }
207
+ deps.stderr(usage(td));
208
+ return 2;
209
+ }
210
+ catch (error) {
211
+ deps.stderr(`crew-daemon: ${td(error.message)}\n`);
212
+ return 1;
213
+ }
214
+ }
@@ -0,0 +1,195 @@
1
+ import { access, chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { spawn } from "node:child_process";
7
+ import { z } from "zod";
8
+ const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
9
+ const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
10
+ const url = new URL(value);
11
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
12
+ }, "serverUrl must be an HTTP(S) URL without embedded credentials");
13
+ const ProfileSchema = z.object({
14
+ version: z.literal(1),
15
+ name: z.string().regex(PROFILE_NAME),
16
+ serverUrl: ServerUrlSchema,
17
+ machineToken: z.string().startsWith("sk_machine_").max(4096),
18
+ agentsRoot: z.string().min(1).optional(),
19
+ 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(),
20
+ }).strict();
21
+ const StoredPlainProfileSchema = ProfileSchema;
22
+ const StoredProtectedProfileSchema = ProfileSchema.omit({ machineToken: true }).extend({
23
+ machineTokenProtected: z.object({
24
+ scheme: z.literal("dpapi-current-user"),
25
+ ciphertext: z.string().min(1).max(16384),
26
+ }).strict(),
27
+ }).strict();
28
+ const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
29
+ export function daemonHome(env = process.env) {
30
+ return resolve(env.CREW_DAEMON_HOME ?? resolve(homedir(), ".crew/daemon"));
31
+ }
32
+ export function validateProfileName(name) {
33
+ if (!PROFILE_NAME.test(name)) {
34
+ throw new Error("profile name must match [a-z0-9][a-z0-9_-]{0,47}");
35
+ }
36
+ return name;
37
+ }
38
+ export function profilePath(name, home = daemonHome()) {
39
+ return resolve(home, "profiles", `${validateProfileName(name)}.json`);
40
+ }
41
+ async function atomicPrivateWrite(path, content, beforeCommit) {
42
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
43
+ await chmod(dirname(path), 0o700);
44
+ const temporary = `${path}.${randomUUID()}.tmp`;
45
+ try {
46
+ await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
47
+ await beforeCommit?.(temporary);
48
+ await rename(temporary, path);
49
+ await chmod(path, 0o600);
50
+ }
51
+ finally {
52
+ await rm(temporary, { force: true });
53
+ }
54
+ }
55
+ const DPAPI_PROTECT = "$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)";
56
+ const DPAPI_UNPROTECT = "$p=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
57
+ async function powershellStdin(script, input) {
58
+ return new Promise((resolvePromise, reject) => {
59
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ windowsHide: true,
62
+ });
63
+ const stdout = [];
64
+ const stderr = [];
65
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
66
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
67
+ child.once("error", reject);
68
+ child.once("close", (code) => {
69
+ if (code !== 0) {
70
+ reject(new Error(`Windows DPAPI operation failed: ${Buffer.concat(stderr).toString("utf8").trim() || `exit ${code}`}`));
71
+ return;
72
+ }
73
+ resolvePromise(Buffer.concat(stdout).toString("utf8").trim());
74
+ });
75
+ child.stdin.end(input);
76
+ });
77
+ }
78
+ export const windowsDpapiProtector = {
79
+ protect: (plaintext) => powershellStdin(DPAPI_PROTECT, plaintext),
80
+ unprotect: (ciphertext) => powershellStdin(DPAPI_UNPROTECT, ciphertext),
81
+ };
82
+ function storageOptions(options) {
83
+ const platform = options.platform ?? process.platform;
84
+ return {
85
+ platform,
86
+ ...(options.protector ? { protector: options.protector } : platform === "win32" ? { protector: windowsDpapiProtector } : {}),
87
+ ...(options.harden ? { harden: options.harden } : {}),
88
+ };
89
+ }
90
+ export async function saveProfile(input, home = daemonHome(), options = {}) {
91
+ const profile = ProfileSchema.parse({ version: 1, ...input });
92
+ const configured = storageOptions(options);
93
+ if (configured.platform === "win32") {
94
+ if (!configured.protector)
95
+ throw new Error("Windows profile storage requires CurrentUser DPAPI");
96
+ if (!configured.harden)
97
+ throw new Error("Windows profile storage requires ACL hardening");
98
+ const ciphertext = await configured.protector.protect(profile.machineToken);
99
+ if (!ciphertext)
100
+ throw new Error("Windows DPAPI returned an empty ciphertext");
101
+ const stored = StoredProtectedProfileSchema.parse({
102
+ version: 1,
103
+ name: profile.name,
104
+ serverUrl: profile.serverUrl,
105
+ ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
106
+ ...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
107
+ machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
108
+ });
109
+ await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
110
+ }
111
+ else {
112
+ await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(profile, null, 2)}\n`);
113
+ }
114
+ return profile;
115
+ }
116
+ export async function loadProfile(name, home = daemonHome(), options = {}) {
117
+ const path = profilePath(name, home);
118
+ try {
119
+ const stored = StoredProfileSchema.parse(JSON.parse(await readFile(path, "utf8")));
120
+ const configured = storageOptions(options);
121
+ if (configured.platform === "win32") {
122
+ if (!("machineTokenProtected" in stored)) {
123
+ throw new Error(`Computer profile '${name}' contains a plaintext token and is refused on Windows`);
124
+ }
125
+ if (!configured.protector)
126
+ throw new Error("Windows profile storage requires CurrentUser DPAPI");
127
+ return ProfileSchema.parse({
128
+ version: stored.version,
129
+ name: stored.name,
130
+ serverUrl: stored.serverUrl,
131
+ ...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
132
+ ...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
133
+ machineToken: await configured.protector.unprotect(stored.machineTokenProtected.ciphertext),
134
+ });
135
+ }
136
+ if (!("machineToken" in stored)) {
137
+ throw new Error(`Computer profile '${name}' is DPAPI-protected and can only be loaded by its Windows user`);
138
+ }
139
+ return stored;
140
+ }
141
+ catch (error) {
142
+ if (error.code === "ENOENT") {
143
+ throw new Error(`Computer profile '${name}' does not exist`);
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ export async function listProfiles(home = daemonHome()) {
149
+ const root = resolve(home, "profiles");
150
+ try {
151
+ const entries = await readdir(root, { withFileTypes: true });
152
+ return entries
153
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
154
+ .map((entry) => entry.name.slice(0, -5))
155
+ .filter((name) => PROFILE_NAME.test(name))
156
+ .sort();
157
+ }
158
+ catch (error) {
159
+ if (error.code === "ENOENT")
160
+ return [];
161
+ throw error;
162
+ }
163
+ }
164
+ export async function removeProfile(name, home = daemonHome()) {
165
+ await rm(profilePath(name, home), { force: true });
166
+ }
167
+ export async function profileIsPrivate(name, home = daemonHome(), platform = process.platform) {
168
+ const path = profilePath(name, home);
169
+ await access(path, constants.R_OK);
170
+ if (platform === "win32") {
171
+ const stored = StoredProfileSchema.parse(JSON.parse(await readFile(path, "utf8")));
172
+ return "machineTokenProtected" in stored && stored.machineTokenProtected.scheme === "dpapi-current-user";
173
+ }
174
+ const { mode } = await stat(path);
175
+ return (mode & 0o077) === 0;
176
+ }
177
+ export function publicProfile(profile) {
178
+ return {
179
+ version: profile.version,
180
+ name: profile.name,
181
+ serverUrl: profile.serverUrl,
182
+ machineTokenConfigured: true,
183
+ ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
184
+ };
185
+ }
186
+ export function applyProfileToEnv(profile, env) {
187
+ env.CREW_SERVER_URL = profile.serverUrl;
188
+ env.CREW_MACHINE_TOKEN = profile.machineToken;
189
+ if (profile.agentsRoot)
190
+ env.CREW_AGENTS_ROOT = profile.agentsRoot;
191
+ else
192
+ delete env.CREW_AGENTS_ROOT;
193
+ if (profile.runtimePath)
194
+ env.PATH = profile.runtimePath;
195
+ }