crewly 1.20.123 → 1.20.124
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/dist/backend/backend/src/constants.d.ts +118 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +118 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/harness/harness.controller.d.ts +54 -0
- package/dist/backend/backend/src/controllers/harness/harness.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/harness/harness.controller.js +204 -0
- package/dist/backend/backend/src/controllers/harness/harness.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts +27 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.js +43 -0
- package/dist/backend/backend/src/controllers/harness/harness.routes.js.map +1 -0
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +5 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +7 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +2 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js +4 -3
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +14 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/harness/claude-config.utils.d.ts +67 -0
- package/dist/backend/backend/src/services/harness/claude-config.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/claude-config.utils.js +118 -0
- package/dist/backend/backend/src/services/harness/claude-config.utils.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-api-key.service.d.ts +99 -0
- package/dist/backend/backend/src/services/harness/harness-api-key.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-api-key.service.js +157 -0
- package/dist/backend/backend/src/services/harness/harness-api-key.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts +135 -0
- package/dist/backend/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-credentials.store.js +208 -0
- package/dist/backend/backend/src/services/harness/harness-credentials.store.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-exec.utils.d.ts +85 -0
- package/dist/backend/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-exec.utils.js +183 -0
- package/dist/backend/backend/src/services/harness/harness-exec.utils.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-install.service.d.ts +106 -0
- package/dist/backend/backend/src/services/harness/harness-install.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-install.service.js +205 -0
- package/dist/backend/backend/src/services/harness/harness-install.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-registry.d.ts +86 -0
- package/dist/backend/backend/src/services/harness/harness-registry.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-registry.js +121 -0
- package/dist/backend/backend/src/services/harness/harness-registry.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-status.service.d.ts +173 -0
- package/dist/backend/backend/src/services/harness/harness-status.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness-status.service.js +324 -0
- package/dist/backend/backend/src/services/harness/harness-status.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness.service.d.ts +146 -0
- package/dist/backend/backend/src/services/harness/harness.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness.service.js +193 -0
- package/dist/backend/backend/src/services/harness/harness.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/harness.types.d.ts +138 -0
- package/dist/backend/backend/src/services/harness/harness.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/harness.types.js +51 -0
- package/dist/backend/backend/src/services/harness/harness.types.js.map +1 -0
- package/dist/backend/backend/src/services/harness/login-broker.service.d.ts +238 -0
- package/dist/backend/backend/src/services/harness/login-broker.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/login-broker.service.js +495 -0
- package/dist/backend/backend/src/services/harness/login-broker.service.js.map +1 -0
- package/dist/backend/backend/src/services/harness/login-rules.d.ts +162 -0
- package/dist/backend/backend/src/services/harness/login-rules.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/login-rules.js +344 -0
- package/dist/backend/backend/src/services/harness/login-rules.js.map +1 -0
- package/dist/backend/backend/src/services/harness/orc-harness.store.d.ts +59 -0
- package/dist/backend/backend/src/services/harness/orc-harness.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/harness/orc-harness.store.js +108 -0
- package/dist/backend/backend/src/services/harness/orc-harness.store.js.map +1 -0
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +118 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +118 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +5 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +7 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +2 -1
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +4 -3
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/cli/backend/src/services/harness/claude-config.utils.d.ts +67 -0
- package/dist/cli/backend/src/services/harness/claude-config.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/claude-config.utils.js +118 -0
- package/dist/cli/backend/src/services/harness/claude-config.utils.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-api-key.service.d.ts +99 -0
- package/dist/cli/backend/src/services/harness/harness-api-key.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-api-key.service.js +157 -0
- package/dist/cli/backend/src/services/harness/harness-api-key.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts +135 -0
- package/dist/cli/backend/src/services/harness/harness-credentials.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-credentials.store.js +208 -0
- package/dist/cli/backend/src/services/harness/harness-credentials.store.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-exec.utils.d.ts +85 -0
- package/dist/cli/backend/src/services/harness/harness-exec.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-exec.utils.js +183 -0
- package/dist/cli/backend/src/services/harness/harness-exec.utils.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-install.service.d.ts +106 -0
- package/dist/cli/backend/src/services/harness/harness-install.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-install.service.js +205 -0
- package/dist/cli/backend/src/services/harness/harness-install.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-registry.d.ts +86 -0
- package/dist/cli/backend/src/services/harness/harness-registry.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-registry.js +121 -0
- package/dist/cli/backend/src/services/harness/harness-registry.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-status.service.d.ts +173 -0
- package/dist/cli/backend/src/services/harness/harness-status.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness-status.service.js +324 -0
- package/dist/cli/backend/src/services/harness/harness-status.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness.service.d.ts +146 -0
- package/dist/cli/backend/src/services/harness/harness.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness.service.js +193 -0
- package/dist/cli/backend/src/services/harness/harness.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/harness.types.d.ts +138 -0
- package/dist/cli/backend/src/services/harness/harness.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/harness.types.js +51 -0
- package/dist/cli/backend/src/services/harness/harness.types.js.map +1 -0
- package/dist/cli/backend/src/services/harness/login-broker.service.d.ts +238 -0
- package/dist/cli/backend/src/services/harness/login-broker.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/login-broker.service.js +495 -0
- package/dist/cli/backend/src/services/harness/login-broker.service.js.map +1 -0
- package/dist/cli/backend/src/services/harness/login-rules.d.ts +162 -0
- package/dist/cli/backend/src/services/harness/login-rules.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/login-rules.js +344 -0
- package/dist/cli/backend/src/services/harness/login-rules.js.map +1 -0
- package/dist/cli/backend/src/services/harness/orc-harness.store.d.ts +59 -0
- package/dist/cli/backend/src/services/harness/orc-harness.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/harness/orc-harness.store.js +108 -0
- package/dist/cli/backend/src/services/harness/orc-harness.store.js.map +1 -0
- package/dist/cli/cli/src/commands/harness-setup.d.ts +159 -0
- package/dist/cli/cli/src/commands/harness-setup.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/harness-setup.js +377 -0
- package/dist/cli/cli/src/commands/harness-setup.js.map +1 -0
- package/dist/cli/cli/src/commands/harness.d.ts +56 -0
- package/dist/cli/cli/src/commands/harness.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/harness.js +97 -0
- package/dist/cli/cli/src/commands/harness.js.map +1 -0
- package/dist/cli/cli/src/commands/onboard.d.ts +108 -50
- package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/onboard.js +200 -191
- package/dist/cli/cli/src/commands/onboard.js.map +1 -1
- package/dist/cli/cli/src/constants.d.ts +18 -0
- package/dist/cli/cli/src/constants.d.ts.map +1 -1
- package/dist/cli/cli/src/constants.js +18 -0
- package/dist/cli/cli/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +22 -3
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/harness-engine.d.ts +99 -0
- package/dist/cli/cli/src/utils/harness-engine.d.ts.map +1 -0
- package/dist/cli/cli/src/utils/harness-engine.js +143 -0
- package/dist/cli/cli/src/utils/harness-engine.js.map +1 -0
- package/dist/cli/cli/src/utils/prompt-io.d.ts +25 -0
- package/dist/cli/cli/src/utils/prompt-io.d.ts.map +1 -0
- package/dist/cli/cli/src/utils/prompt-io.js +47 -0
- package/dist/cli/cli/src/utils/prompt-io.js.map +1 -0
- package/frontend/dist/assets/{index-8327e63e.css → index-0a2f783c.css} +1 -1
- package/frontend/dist/assets/{index-daf43f1a.js → index-aaf65055.js} +309 -309
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness credentials store — the harness logins Crewly itself holds.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `setup-token` prints a long-lived OAuth token instead of
|
|
5
|
+
* saving it, and an Anthropic API key has to reach agents somehow; both live
|
|
6
|
+
* here, in `<crewlyHome>/harness-credentials.json` with mode 0600. Codex
|
|
7
|
+
* writes its own credentials (`$CODEX_HOME/auth.json`), so nothing is stored
|
|
8
|
+
* for it unless a future flow needs the key.
|
|
9
|
+
*
|
|
10
|
+
* {@link harnessEnvForAgents} turns the stored credentials into the env vars
|
|
11
|
+
* injected into every agent PTY (and adds the user npm prefix to PATH).
|
|
12
|
+
*
|
|
13
|
+
* Secrets are never logged, never returned by the REST API and never put in
|
|
14
|
+
* an error message.
|
|
15
|
+
*
|
|
16
|
+
* @module services/harness/harness-credentials.store
|
|
17
|
+
*/
|
|
18
|
+
/** Stored credentials. Every field is optional. */
|
|
19
|
+
export interface HarnessCredentials {
|
|
20
|
+
claude?: {
|
|
21
|
+
/** Long-lived token from `claude setup-token` (exported as CLAUDE_CODE_OAUTH_TOKEN) */
|
|
22
|
+
oauthToken?: string;
|
|
23
|
+
/** Anthropic API key (exported as ANTHROPIC_API_KEY) */
|
|
24
|
+
anthropicApiKey?: string;
|
|
25
|
+
updatedAt?: string;
|
|
26
|
+
};
|
|
27
|
+
codex?: {
|
|
28
|
+
/** OpenAI API key — only kept if a flow needs it (Codex stores its own login) */
|
|
29
|
+
openaiApiKey?: string;
|
|
30
|
+
updatedAt?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Which stored Claude credential is active. */
|
|
34
|
+
export type StoredClaudeCredential = 'oauth_token' | 'api_key' | null;
|
|
35
|
+
/**
|
|
36
|
+
* Reads and writes `harness-credentials.json`.
|
|
37
|
+
*
|
|
38
|
+
* Reads the file on every call (no cache), so the CLI and a running backend
|
|
39
|
+
* always see each other's writes.
|
|
40
|
+
*/
|
|
41
|
+
export declare class HarnessCredentialsStore {
|
|
42
|
+
private readonly filePath?;
|
|
43
|
+
/**
|
|
44
|
+
* @param filePath - Credentials file; defaults to `<crewlyHome>/harness-credentials.json`
|
|
45
|
+
* (resolved per call so `CREWLY_HOME` changes in tests are honoured)
|
|
46
|
+
*/
|
|
47
|
+
constructor(filePath?: string | undefined);
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the credentials file.
|
|
50
|
+
*
|
|
51
|
+
* @returns The path
|
|
52
|
+
*/
|
|
53
|
+
getFilePath(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Read the stored credentials.
|
|
56
|
+
*
|
|
57
|
+
* @returns The credentials; an empty object when the file is missing or unreadable
|
|
58
|
+
*/
|
|
59
|
+
read(): HarnessCredentials;
|
|
60
|
+
/**
|
|
61
|
+
* Write credentials atomically with mode 0600.
|
|
62
|
+
*
|
|
63
|
+
* The temp file is created with 0600 before any secret is written to it,
|
|
64
|
+
* then renamed over the target, and the target is chmod-ed again in case
|
|
65
|
+
* it pre-existed with a wider mode.
|
|
66
|
+
*
|
|
67
|
+
* @param credentials - Full credentials object
|
|
68
|
+
*/
|
|
69
|
+
write(credentials: HarnessCredentials): void;
|
|
70
|
+
/**
|
|
71
|
+
* Store Claude's long-lived OAuth token. Replaces a stored API key, so
|
|
72
|
+
* agents use exactly the login the owner chose last.
|
|
73
|
+
*
|
|
74
|
+
* @param token - Token printed by `claude setup-token`
|
|
75
|
+
* @throws Error when the token is empty
|
|
76
|
+
*/
|
|
77
|
+
setClaudeOauthToken(token: string): void;
|
|
78
|
+
/**
|
|
79
|
+
* Store an Anthropic API key. Replaces a stored OAuth token.
|
|
80
|
+
*
|
|
81
|
+
* @param key - Anthropic API key
|
|
82
|
+
* @throws Error when the key is empty
|
|
83
|
+
*/
|
|
84
|
+
setAnthropicApiKey(key: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* Store an OpenAI API key (only when a flow needs Crewly to hold it).
|
|
87
|
+
*
|
|
88
|
+
* @param key - OpenAI API key
|
|
89
|
+
* @throws Error when the key is empty
|
|
90
|
+
*/
|
|
91
|
+
setOpenaiApiKey(key: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Forget the stored Claude credential.
|
|
94
|
+
*/
|
|
95
|
+
clearClaude(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Which Claude credential Crewly holds (never the value).
|
|
98
|
+
*
|
|
99
|
+
* @returns `oauth_token`, `api_key` or null
|
|
100
|
+
*/
|
|
101
|
+
getClaudeCredentialKind(): StoredClaudeCredential;
|
|
102
|
+
/**
|
|
103
|
+
* Env vars to inject into agent sessions.
|
|
104
|
+
*
|
|
105
|
+
* Always sets PATH (user npm prefix bin first, see harness-exec.utils).
|
|
106
|
+
* Adds `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` when Crewly holds
|
|
107
|
+
* a Claude credential. Nothing is added for Codex: it reads its own
|
|
108
|
+
* `auth.json`.
|
|
109
|
+
*
|
|
110
|
+
* @param baseEnv - Environment whose PATH is extended (defaults to process.env)
|
|
111
|
+
* @returns Env map to merge into the agent's spawn env
|
|
112
|
+
*/
|
|
113
|
+
harnessEnvForAgents(baseEnv?: NodeJS.ProcessEnv): Record<string, string>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The default credentials store.
|
|
117
|
+
*
|
|
118
|
+
* @returns Store backed by `<crewlyHome>/harness-credentials.json`
|
|
119
|
+
*/
|
|
120
|
+
export declare function getHarnessCredentialsStore(): HarnessCredentialsStore;
|
|
121
|
+
/**
|
|
122
|
+
* Env vars to inject into agent sessions, from the default store.
|
|
123
|
+
*
|
|
124
|
+
* Never throws: a broken credentials file yields just the PATH.
|
|
125
|
+
*
|
|
126
|
+
* @param baseEnv - Environment whose PATH is extended (defaults to process.env)
|
|
127
|
+
* @returns Env map (PATH plus any Claude credential env var)
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const env = { ...identityEnv, ...harnessEnvForAgents() };
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export declare function harnessEnvForAgents(baseEnv?: NodeJS.ProcessEnv): Record<string, string>;
|
|
135
|
+
//# sourceMappingURL=harness-credentials.store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-credentials.store.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-credentials.store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,mDAAmD;AACnD,MAAM,WAAW,kBAAkB;IAClC,MAAM,CAAC,EAAE;QACR,uFAAuF;QACvF,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,wDAAwD;QACxD,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,CAAC,EAAE;QACP,iFAAiF;QACjF,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACF;AAED,gDAAgD;AAChD,MAAM,MAAM,sBAAsB,GAAG,aAAa,GAAG,SAAS,GAAG,IAAI,CAAC;AAYtE;;;;;GAKG;AACH,qBAAa,uBAAuB;IAKvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAJtC;;;OAGG;gBAC0B,QAAQ,CAAC,EAAE,MAAM,YAAA;IAE9C;;;;OAIG;IACH,WAAW,IAAI,MAAM;IAIrB;;;;OAIG;IACH,IAAI,IAAI,kBAAkB;IAS1B;;;;;;;;OAQG;IACH,KAAK,CAAC,WAAW,EAAE,kBAAkB,GAAG,IAAI;IAe5C;;;;;;OAMG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAMxC;;;;;OAKG;IACH,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMrC;;;;;OAKG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMlC;;OAEG;IACH,WAAW,IAAI,IAAI;IAQnB;;;;OAIG;IACH,uBAAuB,IAAI,sBAAsB;IAOjD;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,OAAO,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAUrF;AAKD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,uBAAuB,CAEpE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMpG"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness credentials store — the harness logins Crewly itself holds.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's `setup-token` prints a long-lived OAuth token instead of
|
|
5
|
+
* saving it, and an Anthropic API key has to reach agents somehow; both live
|
|
6
|
+
* here, in `<crewlyHome>/harness-credentials.json` with mode 0600. Codex
|
|
7
|
+
* writes its own credentials (`$CODEX_HOME/auth.json`), so nothing is stored
|
|
8
|
+
* for it unless a future flow needs the key.
|
|
9
|
+
*
|
|
10
|
+
* {@link harnessEnvForAgents} turns the stored credentials into the env vars
|
|
11
|
+
* injected into every agent PTY (and adds the user npm prefix to PATH).
|
|
12
|
+
*
|
|
13
|
+
* Secrets are never logged, never returned by the REST API and never put in
|
|
14
|
+
* an error message.
|
|
15
|
+
*
|
|
16
|
+
* @module services/harness/harness-credentials.store
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { HARNESS_CONSTANTS } from '../../constants.js';
|
|
21
|
+
import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
|
|
22
|
+
import { buildHarnessPath } from './harness-exec.utils.js';
|
|
23
|
+
/**
|
|
24
|
+
* Whether a value is a non-empty string.
|
|
25
|
+
*
|
|
26
|
+
* @param value - Candidate
|
|
27
|
+
* @returns True for a string with non-whitespace content
|
|
28
|
+
*/
|
|
29
|
+
function isNonEmptyString(value) {
|
|
30
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads and writes `harness-credentials.json`.
|
|
34
|
+
*
|
|
35
|
+
* Reads the file on every call (no cache), so the CLI and a running backend
|
|
36
|
+
* always see each other's writes.
|
|
37
|
+
*/
|
|
38
|
+
export class HarnessCredentialsStore {
|
|
39
|
+
filePath;
|
|
40
|
+
/**
|
|
41
|
+
* @param filePath - Credentials file; defaults to `<crewlyHome>/harness-credentials.json`
|
|
42
|
+
* (resolved per call so `CREWLY_HOME` changes in tests are honoured)
|
|
43
|
+
*/
|
|
44
|
+
constructor(filePath) {
|
|
45
|
+
this.filePath = filePath;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Absolute path of the credentials file.
|
|
49
|
+
*
|
|
50
|
+
* @returns The path
|
|
51
|
+
*/
|
|
52
|
+
getFilePath() {
|
|
53
|
+
return this.filePath ?? path.join(getCrewlyHomePath(), HARNESS_CONSTANTS.CREDENTIALS_FILE);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read the stored credentials.
|
|
57
|
+
*
|
|
58
|
+
* @returns The credentials; an empty object when the file is missing or unreadable
|
|
59
|
+
*/
|
|
60
|
+
read() {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(fs.readFileSync(this.getFilePath(), 'utf-8'));
|
|
63
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write credentials atomically with mode 0600.
|
|
71
|
+
*
|
|
72
|
+
* The temp file is created with 0600 before any secret is written to it,
|
|
73
|
+
* then renamed over the target, and the target is chmod-ed again in case
|
|
74
|
+
* it pre-existed with a wider mode.
|
|
75
|
+
*
|
|
76
|
+
* @param credentials - Full credentials object
|
|
77
|
+
*/
|
|
78
|
+
write(credentials) {
|
|
79
|
+
const file = this.getFilePath();
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
82
|
+
const fd = fs.openSync(tmp, 'w', HARNESS_CONSTANTS.CREDENTIALS_FILE_MODE);
|
|
83
|
+
try {
|
|
84
|
+
fs.writeSync(fd, `${JSON.stringify(credentials, null, 2)}\n`);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
fs.closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
fs.chmodSync(tmp, HARNESS_CONSTANTS.CREDENTIALS_FILE_MODE);
|
|
90
|
+
fs.renameSync(tmp, file);
|
|
91
|
+
fs.chmodSync(file, HARNESS_CONSTANTS.CREDENTIALS_FILE_MODE);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Store Claude's long-lived OAuth token. Replaces a stored API key, so
|
|
95
|
+
* agents use exactly the login the owner chose last.
|
|
96
|
+
*
|
|
97
|
+
* @param token - Token printed by `claude setup-token`
|
|
98
|
+
* @throws Error when the token is empty
|
|
99
|
+
*/
|
|
100
|
+
setClaudeOauthToken(token) {
|
|
101
|
+
if (!isNonEmptyString(token))
|
|
102
|
+
throw new Error('Claude OAuth token is empty');
|
|
103
|
+
const current = this.read();
|
|
104
|
+
this.write({ ...current, claude: { oauthToken: token.trim(), updatedAt: new Date().toISOString() } });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Store an Anthropic API key. Replaces a stored OAuth token.
|
|
108
|
+
*
|
|
109
|
+
* @param key - Anthropic API key
|
|
110
|
+
* @throws Error when the key is empty
|
|
111
|
+
*/
|
|
112
|
+
setAnthropicApiKey(key) {
|
|
113
|
+
if (!isNonEmptyString(key))
|
|
114
|
+
throw new Error('Anthropic API key is empty');
|
|
115
|
+
const current = this.read();
|
|
116
|
+
this.write({ ...current, claude: { anthropicApiKey: key.trim(), updatedAt: new Date().toISOString() } });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Store an OpenAI API key (only when a flow needs Crewly to hold it).
|
|
120
|
+
*
|
|
121
|
+
* @param key - OpenAI API key
|
|
122
|
+
* @throws Error when the key is empty
|
|
123
|
+
*/
|
|
124
|
+
setOpenaiApiKey(key) {
|
|
125
|
+
if (!isNonEmptyString(key))
|
|
126
|
+
throw new Error('OpenAI API key is empty');
|
|
127
|
+
const current = this.read();
|
|
128
|
+
this.write({ ...current, codex: { openaiApiKey: key.trim(), updatedAt: new Date().toISOString() } });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Forget the stored Claude credential.
|
|
132
|
+
*/
|
|
133
|
+
clearClaude() {
|
|
134
|
+
const current = this.read();
|
|
135
|
+
if (!current.claude)
|
|
136
|
+
return;
|
|
137
|
+
const next = { ...current };
|
|
138
|
+
delete next.claude;
|
|
139
|
+
this.write(next);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Which Claude credential Crewly holds (never the value).
|
|
143
|
+
*
|
|
144
|
+
* @returns `oauth_token`, `api_key` or null
|
|
145
|
+
*/
|
|
146
|
+
getClaudeCredentialKind() {
|
|
147
|
+
const claude = this.read().claude;
|
|
148
|
+
if (isNonEmptyString(claude?.oauthToken))
|
|
149
|
+
return 'oauth_token';
|
|
150
|
+
if (isNonEmptyString(claude?.anthropicApiKey))
|
|
151
|
+
return 'api_key';
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Env vars to inject into agent sessions.
|
|
156
|
+
*
|
|
157
|
+
* Always sets PATH (user npm prefix bin first, see harness-exec.utils).
|
|
158
|
+
* Adds `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` when Crewly holds
|
|
159
|
+
* a Claude credential. Nothing is added for Codex: it reads its own
|
|
160
|
+
* `auth.json`.
|
|
161
|
+
*
|
|
162
|
+
* @param baseEnv - Environment whose PATH is extended (defaults to process.env)
|
|
163
|
+
* @returns Env map to merge into the agent's spawn env
|
|
164
|
+
*/
|
|
165
|
+
harnessEnvForAgents(baseEnv = process.env) {
|
|
166
|
+
const env = { PATH: buildHarnessPath(baseEnv.PATH) };
|
|
167
|
+
const claude = this.read().claude;
|
|
168
|
+
if (isNonEmptyString(claude?.oauthToken)) {
|
|
169
|
+
env[HARNESS_CONSTANTS.CLAUDE.OAUTH_TOKEN_ENV] = claude.oauthToken;
|
|
170
|
+
}
|
|
171
|
+
else if (isNonEmptyString(claude?.anthropicApiKey)) {
|
|
172
|
+
env[HARNESS_CONSTANTS.CLAUDE.API_KEY_ENV] = claude.anthropicApiKey;
|
|
173
|
+
}
|
|
174
|
+
return env;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Process-wide default store (path resolved per call from CREWLY_HOME). */
|
|
178
|
+
const defaultStore = new HarnessCredentialsStore();
|
|
179
|
+
/**
|
|
180
|
+
* The default credentials store.
|
|
181
|
+
*
|
|
182
|
+
* @returns Store backed by `<crewlyHome>/harness-credentials.json`
|
|
183
|
+
*/
|
|
184
|
+
export function getHarnessCredentialsStore() {
|
|
185
|
+
return defaultStore;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Env vars to inject into agent sessions, from the default store.
|
|
189
|
+
*
|
|
190
|
+
* Never throws: a broken credentials file yields just the PATH.
|
|
191
|
+
*
|
|
192
|
+
* @param baseEnv - Environment whose PATH is extended (defaults to process.env)
|
|
193
|
+
* @returns Env map (PATH plus any Claude credential env var)
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const env = { ...identityEnv, ...harnessEnvForAgents() };
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function harnessEnvForAgents(baseEnv = process.env) {
|
|
201
|
+
try {
|
|
202
|
+
return defaultStore.harnessEnvForAgents(baseEnv);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return { PATH: buildHarnessPath(baseEnv.PATH) };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=harness-credentials.store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-credentials.store.js","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-credentials.store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process helpers for harness onboarding: the PATH every harness process gets
|
|
3
|
+
* and a shell-free command runner.
|
|
4
|
+
*
|
|
5
|
+
* PATH: when `npm install -g` hits EACCES, Crewly installs the harness under a
|
|
6
|
+
* user-owned prefix (`<crewlyHome>/npm-global`). Its `bin` dir is prepended to
|
|
7
|
+
* PATH for everything Crewly spawns — status probes, the login broker and agent
|
|
8
|
+
* sessions — so a harness installed there is found without touching the
|
|
9
|
+
* user's shell profile. Claude Code's native installer dir (`~/.local/bin`) is
|
|
10
|
+
* appended when missing, because a backend started by launchd/systemd often
|
|
11
|
+
* lacks it.
|
|
12
|
+
*
|
|
13
|
+
* @module services/harness/harness-exec.utils
|
|
14
|
+
*/
|
|
15
|
+
import type { RunCommand } from './harness.types.js';
|
|
16
|
+
/**
|
|
17
|
+
* The user-owned npm prefix used when a global install is not permitted.
|
|
18
|
+
*
|
|
19
|
+
* @returns Absolute path, e.g. `~/.crewly/npm-global`
|
|
20
|
+
*/
|
|
21
|
+
export declare function getUserNpmPrefix(): string;
|
|
22
|
+
/**
|
|
23
|
+
* The bin directory of the user-owned npm prefix.
|
|
24
|
+
*
|
|
25
|
+
* @returns Absolute path, e.g. `~/.crewly/npm-global/bin`
|
|
26
|
+
*/
|
|
27
|
+
export declare function getUserNpmBinDir(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build the PATH value harness processes run with.
|
|
30
|
+
*
|
|
31
|
+
* Prepends the user npm prefix bin dir and appends `~/.local/bin`, each only
|
|
32
|
+
* when not already present. Order of the existing entries is preserved.
|
|
33
|
+
*
|
|
34
|
+
* @param currentPath - Existing PATH (may be undefined)
|
|
35
|
+
* @param homeDir - Home directory (injectable for tests)
|
|
36
|
+
* @returns The PATH string
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* buildHarnessPath('/usr/bin', '/home/me');
|
|
41
|
+
* // '/home/me/.crewly/npm-global/bin:/usr/bin:/home/me/.local/bin'
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildHarnessPath(currentPath: string | undefined, homeDir?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* PATH for running `npm`: the harness PATH plus the directory of the running
|
|
47
|
+
* Node binary, where npm lives in nvm / installer layouts. A backend started
|
|
48
|
+
* by launchd or systemd often has neither on its PATH.
|
|
49
|
+
*
|
|
50
|
+
* @param currentPath - Existing PATH
|
|
51
|
+
* @param homeDir - Home directory (injectable for tests)
|
|
52
|
+
* @param nodeBinDir - Directory of the Node binary (injectable for tests)
|
|
53
|
+
* @returns The PATH string
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildNpmPath(currentPath: string | undefined, homeDir?: string, nodeBinDir?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Copy an environment with PATH set to {@link buildHarnessPath}.
|
|
58
|
+
*
|
|
59
|
+
* @param env - Source environment (defaults to process.env)
|
|
60
|
+
* @returns A new environment object
|
|
61
|
+
*/
|
|
62
|
+
export declare function withHarnessPath(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
63
|
+
/**
|
|
64
|
+
* Find an executable on a PATH, like `which`.
|
|
65
|
+
*
|
|
66
|
+
* @param command - Bare command name (or an absolute path, returned when executable)
|
|
67
|
+
* @param envPath - PATH to search
|
|
68
|
+
* @param isExecutable - Executable check (injectable for tests)
|
|
69
|
+
* @returns Absolute path, or null when not found
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveExecutable(command: string, envPath: string | undefined, isExecutable?: (file: string) => boolean): string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Run a command without a shell and collect its output.
|
|
74
|
+
*
|
|
75
|
+
* Never rejects: a spawn failure (ENOENT) or timeout comes back as
|
|
76
|
+
* `{ code: null, error }`. Secrets must go through `options.stdin`, never
|
|
77
|
+
* through `args`, so they cannot appear in `ps` output.
|
|
78
|
+
*
|
|
79
|
+
* @param command - Executable
|
|
80
|
+
* @param args - Arguments
|
|
81
|
+
* @param options - env, timeout, stdin, streaming output callback
|
|
82
|
+
* @returns Exit code and output
|
|
83
|
+
*/
|
|
84
|
+
export declare const runCommand: RunCommand;
|
|
85
|
+
//# sourceMappingURL=harness-exec.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-exec.utils.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-exec.utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EAAiB,UAAU,EAAqB,MAAM,oBAAoB,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAOxG;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,GAAE,MAAqB,EAC9B,UAAU,GAAE,MAAuC,GACjD,MAAM,CAGR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,UAAU,CAEvF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,YAAY,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAA6B,GAC3D,MAAM,GAAG,IAAI,CAUf;AAiBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,EAAE,UAsDtB,CAAC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process helpers for harness onboarding: the PATH every harness process gets
|
|
3
|
+
* and a shell-free command runner.
|
|
4
|
+
*
|
|
5
|
+
* PATH: when `npm install -g` hits EACCES, Crewly installs the harness under a
|
|
6
|
+
* user-owned prefix (`<crewlyHome>/npm-global`). Its `bin` dir is prepended to
|
|
7
|
+
* PATH for everything Crewly spawns — status probes, the login broker and agent
|
|
8
|
+
* sessions — so a harness installed there is found without touching the
|
|
9
|
+
* user's shell profile. Claude Code's native installer dir (`~/.local/bin`) is
|
|
10
|
+
* appended when missing, because a backend started by launchd/systemd often
|
|
11
|
+
* lacks it.
|
|
12
|
+
*
|
|
13
|
+
* @module services/harness/harness-exec.utils
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'child_process';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { HARNESS_CONSTANTS } from '../../constants.js';
|
|
20
|
+
import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
|
|
21
|
+
/**
|
|
22
|
+
* The user-owned npm prefix used when a global install is not permitted.
|
|
23
|
+
*
|
|
24
|
+
* @returns Absolute path, e.g. `~/.crewly/npm-global`
|
|
25
|
+
*/
|
|
26
|
+
export function getUserNpmPrefix() {
|
|
27
|
+
return path.join(getCrewlyHomePath(), HARNESS_CONSTANTS.USER_NPM_PREFIX_DIR);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The bin directory of the user-owned npm prefix.
|
|
31
|
+
*
|
|
32
|
+
* @returns Absolute path, e.g. `~/.crewly/npm-global/bin`
|
|
33
|
+
*/
|
|
34
|
+
export function getUserNpmBinDir() {
|
|
35
|
+
return path.join(getUserNpmPrefix(), 'bin');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the PATH value harness processes run with.
|
|
39
|
+
*
|
|
40
|
+
* Prepends the user npm prefix bin dir and appends `~/.local/bin`, each only
|
|
41
|
+
* when not already present. Order of the existing entries is preserved.
|
|
42
|
+
*
|
|
43
|
+
* @param currentPath - Existing PATH (may be undefined)
|
|
44
|
+
* @param homeDir - Home directory (injectable for tests)
|
|
45
|
+
* @returns The PATH string
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* buildHarnessPath('/usr/bin', '/home/me');
|
|
50
|
+
* // '/home/me/.crewly/npm-global/bin:/usr/bin:/home/me/.local/bin'
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function buildHarnessPath(currentPath, homeDir = os.homedir()) {
|
|
54
|
+
const entries = (currentPath ?? '').split(path.delimiter).filter((entry) => entry.length > 0);
|
|
55
|
+
const npmBin = getUserNpmBinDir();
|
|
56
|
+
const nativeBin = path.join(homeDir, HARNESS_CONSTANTS.NATIVE_INSTALLER_BIN_DIR);
|
|
57
|
+
const result = entries.includes(npmBin) ? [...entries] : [npmBin, ...entries];
|
|
58
|
+
if (!result.includes(nativeBin))
|
|
59
|
+
result.push(nativeBin);
|
|
60
|
+
return result.join(path.delimiter);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* PATH for running `npm`: the harness PATH plus the directory of the running
|
|
64
|
+
* Node binary, where npm lives in nvm / installer layouts. A backend started
|
|
65
|
+
* by launchd or systemd often has neither on its PATH.
|
|
66
|
+
*
|
|
67
|
+
* @param currentPath - Existing PATH
|
|
68
|
+
* @param homeDir - Home directory (injectable for tests)
|
|
69
|
+
* @param nodeBinDir - Directory of the Node binary (injectable for tests)
|
|
70
|
+
* @returns The PATH string
|
|
71
|
+
*/
|
|
72
|
+
export function buildNpmPath(currentPath, homeDir = os.homedir(), nodeBinDir = path.dirname(process.execPath)) {
|
|
73
|
+
const harnessPath = buildHarnessPath(currentPath, homeDir);
|
|
74
|
+
return harnessPath.split(path.delimiter).includes(nodeBinDir) ? harnessPath : `${harnessPath}${path.delimiter}${nodeBinDir}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Copy an environment with PATH set to {@link buildHarnessPath}.
|
|
78
|
+
*
|
|
79
|
+
* @param env - Source environment (defaults to process.env)
|
|
80
|
+
* @returns A new environment object
|
|
81
|
+
*/
|
|
82
|
+
export function withHarnessPath(env = process.env) {
|
|
83
|
+
return { ...env, PATH: buildHarnessPath(env.PATH) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Find an executable on a PATH, like `which`.
|
|
87
|
+
*
|
|
88
|
+
* @param command - Bare command name (or an absolute path, returned when executable)
|
|
89
|
+
* @param envPath - PATH to search
|
|
90
|
+
* @param isExecutable - Executable check (injectable for tests)
|
|
91
|
+
* @returns Absolute path, or null when not found
|
|
92
|
+
*/
|
|
93
|
+
export function resolveExecutable(command, envPath, isExecutable = defaultIsExecutable) {
|
|
94
|
+
if (command.includes(path.sep)) {
|
|
95
|
+
return isExecutable(command) ? command : null;
|
|
96
|
+
}
|
|
97
|
+
for (const dir of (envPath ?? '').split(path.delimiter)) {
|
|
98
|
+
if (!dir)
|
|
99
|
+
continue;
|
|
100
|
+
const candidate = path.join(dir, command);
|
|
101
|
+
if (isExecutable(candidate))
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether a file exists and is executable by this user.
|
|
108
|
+
*
|
|
109
|
+
* @param file - Absolute path
|
|
110
|
+
* @returns True when executable
|
|
111
|
+
*/
|
|
112
|
+
function defaultIsExecutable(file) {
|
|
113
|
+
try {
|
|
114
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
115
|
+
return fs.statSync(file).isFile();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Run a command without a shell and collect its output.
|
|
123
|
+
*
|
|
124
|
+
* Never rejects: a spawn failure (ENOENT) or timeout comes back as
|
|
125
|
+
* `{ code: null, error }`. Secrets must go through `options.stdin`, never
|
|
126
|
+
* through `args`, so they cannot appear in `ps` output.
|
|
127
|
+
*
|
|
128
|
+
* @param command - Executable
|
|
129
|
+
* @param args - Arguments
|
|
130
|
+
* @param options - env, timeout, stdin, streaming output callback
|
|
131
|
+
* @returns Exit code and output
|
|
132
|
+
*/
|
|
133
|
+
export const runCommand = (command, args, options = {}) => new Promise((resolve) => {
|
|
134
|
+
let stdout = '';
|
|
135
|
+
let stderr = '';
|
|
136
|
+
let settled = false;
|
|
137
|
+
let timer = null;
|
|
138
|
+
const finish = (result) => {
|
|
139
|
+
if (settled)
|
|
140
|
+
return;
|
|
141
|
+
settled = true;
|
|
142
|
+
if (timer)
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
resolve(result);
|
|
145
|
+
};
|
|
146
|
+
let child;
|
|
147
|
+
try {
|
|
148
|
+
child = spawn(command, [...args], {
|
|
149
|
+
env: options.env ?? process.env,
|
|
150
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
resolve({ code: null, stdout: '', stderr: '', error: error instanceof Error ? error.message : String(error) });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (options.timeoutMs) {
|
|
158
|
+
timer = setTimeout(() => {
|
|
159
|
+
child.kill('SIGKILL');
|
|
160
|
+
finish({ code: null, stdout, stderr, error: `timed out after ${options.timeoutMs} ms` });
|
|
161
|
+
}, options.timeoutMs);
|
|
162
|
+
}
|
|
163
|
+
child.stdout?.on('data', (chunk) => {
|
|
164
|
+
const text = chunk.toString();
|
|
165
|
+
stdout += text;
|
|
166
|
+
options.onOutput?.(text);
|
|
167
|
+
});
|
|
168
|
+
child.stderr?.on('data', (chunk) => {
|
|
169
|
+
const text = chunk.toString();
|
|
170
|
+
stderr += text;
|
|
171
|
+
options.onOutput?.(text);
|
|
172
|
+
});
|
|
173
|
+
child.on('error', (error) => finish({ code: null, stdout, stderr, error: error.message }));
|
|
174
|
+
child.on('close', (code) => finish({ code, stdout, stderr }));
|
|
175
|
+
child.stdin?.on('error', () => undefined);
|
|
176
|
+
if (options.stdin !== undefined) {
|
|
177
|
+
child.stdin?.end(options.stdin);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
child.stdin?.end();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=harness-exec.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-exec.utils.js","sourceRoot":"","sources":["../../../../../../backend/src/services/harness/harness-exec.utils.ts"],"names":[],"mappings":"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"}
|