@metagptx/deepflow-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/bin/deepflow.js +7 -0
- package/docs/cli-design.md +365 -0
- package/package.json +29 -0
- package/src/args.js +198 -0
- package/src/cli.js +531 -0
- package/src/client.js +318 -0
- package/src/config.js +156 -0
- package/src/errors.js +55 -0
- package/src/output.js +87 -0
- package/src/version.js +7 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_BASE_URL, normalizeBaseUrl } from "./config.js";
|
|
4
|
+
import { CliError, exitCodeForHttpStatus } from "./errors.js";
|
|
5
|
+
import { VERSION as CLI_VERSION } from "./version.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
8
|
+
const DEFAULT_FAST_TIMEOUT_MS = 15_000;
|
|
9
|
+
|
|
10
|
+
function parseJson(text) {
|
|
11
|
+
if (!text.trim()) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
} catch {
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summarizePayload(payload) {
|
|
23
|
+
if (!payload || typeof payload !== "object") {
|
|
24
|
+
return payload ? String(payload) : "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
typeof payload.error_code === "string" ? payload.error_code : "",
|
|
29
|
+
typeof payload.summary === "string" ? payload.summary : "",
|
|
30
|
+
payload.context &&
|
|
31
|
+
typeof payload.context === "object" &&
|
|
32
|
+
typeof payload.context.reason === "string"
|
|
33
|
+
? payload.context.reason
|
|
34
|
+
: "",
|
|
35
|
+
]
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join(" - ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAbortError(error) {
|
|
41
|
+
return error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class DeepFlowClient {
|
|
45
|
+
constructor({
|
|
46
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
47
|
+
token = null,
|
|
48
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
49
|
+
fastTimeoutMs = DEFAULT_FAST_TIMEOUT_MS,
|
|
50
|
+
cliVersion = CLI_VERSION,
|
|
51
|
+
} = {}) {
|
|
52
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
53
|
+
this.token = token;
|
|
54
|
+
this.timeoutMs = timeoutMs;
|
|
55
|
+
this.fastTimeoutMs = fastTimeoutMs;
|
|
56
|
+
this.cliVersion = cliVersion;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
withBaseUrl(baseUrl) {
|
|
60
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
buildHeaders(body, { auditMethod } = {}) {
|
|
65
|
+
const headers = new Headers();
|
|
66
|
+
|
|
67
|
+
headers.set("accept", "application/json");
|
|
68
|
+
|
|
69
|
+
if (this.cliVersion) {
|
|
70
|
+
headers.set("x-deepflow-cli-version", this.cliVersion);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (auditMethod) {
|
|
74
|
+
headers.set("x-deepflow-cli-method", auditMethod);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (body !== undefined) {
|
|
78
|
+
headers.set("content-type", "application/json");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.token) {
|
|
82
|
+
headers.set("authorization", `Bearer ${this.token}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return headers;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async request(path, {
|
|
89
|
+
method = "GET",
|
|
90
|
+
body,
|
|
91
|
+
signal,
|
|
92
|
+
timeoutMs = this.timeoutMs,
|
|
93
|
+
auditMethod,
|
|
94
|
+
} = {}) {
|
|
95
|
+
const url = new URL(path, this.baseUrl);
|
|
96
|
+
const requestBody = body === undefined ? undefined : JSON.stringify(body);
|
|
97
|
+
let response;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
response = await fetch(url, {
|
|
101
|
+
method,
|
|
102
|
+
body: requestBody,
|
|
103
|
+
headers: this.buildHeaders(requestBody, { auditMethod }),
|
|
104
|
+
signal: signal
|
|
105
|
+
? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
106
|
+
: AbortSignal.timeout(timeoutMs),
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (isAbortError(error)) {
|
|
110
|
+
throw new CliError(`DeepFlow Web did not respond at ${url.toString()} within ${timeoutMs}ms.`, {
|
|
111
|
+
code: "web_timeout",
|
|
112
|
+
exitCode: 5,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new CliError(`Failed to connect to DeepFlow Web at ${url.toString()}: ${error.message}`, {
|
|
117
|
+
code: "web_unreachable",
|
|
118
|
+
exitCode: 5,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const text = await response.text();
|
|
123
|
+
const payload = parseJson(text);
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const details = summarizePayload(payload);
|
|
127
|
+
|
|
128
|
+
throw new CliError(
|
|
129
|
+
`${method} ${url.pathname}${url.search} failed with HTTP ${response.status}${
|
|
130
|
+
details ? `: ${details}` : ""
|
|
131
|
+
}`,
|
|
132
|
+
{
|
|
133
|
+
code: payload?.error_code || "http_error",
|
|
134
|
+
exitCode: exitCodeForHttpStatus(response.status, payload),
|
|
135
|
+
status: response.status,
|
|
136
|
+
payload,
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async authConfig(options = {}) {
|
|
145
|
+
return this.request("/api/auth/config", {
|
|
146
|
+
timeoutMs: this.fastTimeoutMs,
|
|
147
|
+
...options,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async verifyToken(auditMethod = "auth.verify") {
|
|
152
|
+
return this.request("/api/auth/token/verify", {
|
|
153
|
+
timeoutMs: this.fastTimeoutMs,
|
|
154
|
+
auditMethod,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async listApps() {
|
|
159
|
+
const payload = await this.request("/api/local/apps", {
|
|
160
|
+
timeoutMs: this.fastTimeoutMs,
|
|
161
|
+
auditMethod: "apps.list",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return Array.isArray(payload?.items) ? payload.items : [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async createIteration(input) {
|
|
168
|
+
return this.request("/api/local/iterations", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: input,
|
|
171
|
+
auditMethod: "plan.run",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async createPlanSession(input) {
|
|
176
|
+
return this.request("/api/local/sessions", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: {
|
|
179
|
+
...input,
|
|
180
|
+
stage: "plan",
|
|
181
|
+
},
|
|
182
|
+
auditMethod: "plan.run",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async sendPlanMessage({
|
|
187
|
+
localIterationId,
|
|
188
|
+
sessionId,
|
|
189
|
+
prompt,
|
|
190
|
+
}) {
|
|
191
|
+
return this.request(
|
|
192
|
+
`/api/local/sessions/${encodeURIComponent(sessionId)}/messages?iterationId=${encodeURIComponent(localIterationId)}`,
|
|
193
|
+
{
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: {
|
|
196
|
+
message_id: `cli-${Date.now()}-${randomUUID()}`,
|
|
197
|
+
mode: "plan",
|
|
198
|
+
content: prompt,
|
|
199
|
+
},
|
|
200
|
+
auditMethod: "plan.run",
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getLocalSession({
|
|
206
|
+
localIterationId,
|
|
207
|
+
sessionId,
|
|
208
|
+
}) {
|
|
209
|
+
return this.request(
|
|
210
|
+
`/api/local/sessions/${encodeURIComponent(sessionId)}?iterationId=${encodeURIComponent(localIterationId)}`,
|
|
211
|
+
{
|
|
212
|
+
timeoutMs: this.fastTimeoutMs,
|
|
213
|
+
auditMethod: "plan.status",
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async listMessages({
|
|
219
|
+
backendIterationId,
|
|
220
|
+
sessionId,
|
|
221
|
+
}) {
|
|
222
|
+
return this.request(
|
|
223
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages?iteration_id=${encodeURIComponent(backendIterationId)}`,
|
|
224
|
+
{
|
|
225
|
+
timeoutMs: this.fastTimeoutMs,
|
|
226
|
+
auditMethod: "session.messages",
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async stopSession({
|
|
232
|
+
backendIterationId,
|
|
233
|
+
sessionId,
|
|
234
|
+
}) {
|
|
235
|
+
return this.request(
|
|
236
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/stop?iteration_id=${encodeURIComponent(backendIterationId)}`,
|
|
237
|
+
{
|
|
238
|
+
method: "POST",
|
|
239
|
+
timeoutMs: this.fastTimeoutMs,
|
|
240
|
+
auditMethod: "session.stop",
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function buildPlanStatusUrl(baseUrl, {
|
|
247
|
+
localIterationId,
|
|
248
|
+
sessionId,
|
|
249
|
+
}) {
|
|
250
|
+
return new URL(
|
|
251
|
+
`/api/local/sessions/${encodeURIComponent(sessionId)}?iterationId=${encodeURIComponent(localIterationId)}`,
|
|
252
|
+
baseUrl,
|
|
253
|
+
).toString();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildSessionMessagesUrl(baseUrl, {
|
|
257
|
+
backendIterationId,
|
|
258
|
+
sessionId,
|
|
259
|
+
}) {
|
|
260
|
+
return new URL(
|
|
261
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages?iteration_id=${encodeURIComponent(backendIterationId)}`,
|
|
262
|
+
baseUrl,
|
|
263
|
+
).toString();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function buildSessionStopUrl(baseUrl, {
|
|
267
|
+
backendIterationId,
|
|
268
|
+
sessionId,
|
|
269
|
+
}) {
|
|
270
|
+
return new URL(
|
|
271
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/stop?iteration_id=${encodeURIComponent(backendIterationId)}`,
|
|
272
|
+
baseUrl,
|
|
273
|
+
).toString();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function resolveReachableBaseUrl(client) {
|
|
277
|
+
const parsed = new URL(client.baseUrl);
|
|
278
|
+
|
|
279
|
+
if (parsed.hostname === "127.0.0.1") {
|
|
280
|
+
parsed.hostname = "localhost";
|
|
281
|
+
const fallbackBaseUrl = normalizeBaseUrl(parsed.toString());
|
|
282
|
+
const fallbackClient = new DeepFlowClient({
|
|
283
|
+
baseUrl: fallbackBaseUrl,
|
|
284
|
+
token: client.token,
|
|
285
|
+
timeoutMs: client.timeoutMs,
|
|
286
|
+
fastTimeoutMs: client.fastTimeoutMs,
|
|
287
|
+
cliVersion: client.cliVersion,
|
|
288
|
+
});
|
|
289
|
+
const primaryController = new AbortController();
|
|
290
|
+
const fallbackController = new AbortController();
|
|
291
|
+
|
|
292
|
+
const reachableBaseUrl = await Promise.any([
|
|
293
|
+
client.authConfig({ signal: primaryController.signal }).then(() => client.baseUrl),
|
|
294
|
+
fallbackClient.authConfig({ signal: fallbackController.signal }).then(() => fallbackBaseUrl),
|
|
295
|
+
])
|
|
296
|
+
.catch((error) => {
|
|
297
|
+
throw error.errors?.[0] ?? error;
|
|
298
|
+
})
|
|
299
|
+
.finally(() => {
|
|
300
|
+
primaryController.abort();
|
|
301
|
+
fallbackController.abort();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
client.withBaseUrl(reachableBaseUrl);
|
|
305
|
+
return reachableBaseUrl;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await client.authConfig();
|
|
309
|
+
return client.baseUrl;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function parseTimeoutMs(value, fallback) {
|
|
313
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
314
|
+
|
|
315
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { DEFAULT_FAST_TIMEOUT_MS, DEFAULT_TIMEOUT_MS };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_BASE_URL = "http://localhost:3100";
|
|
8
|
+
export const DEFAULT_PROFILE = "default";
|
|
9
|
+
|
|
10
|
+
export function resolveConfigPath(env = process.env) {
|
|
11
|
+
if (env.DEEPFLOW_CONFIG) {
|
|
12
|
+
return path.resolve(env.DEEPFLOW_CONFIG);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const configDir = env.DEEPFLOW_CONFIG_DIR
|
|
16
|
+
? path.resolve(env.DEEPFLOW_CONFIG_DIR)
|
|
17
|
+
: path.join(os.homedir(), ".deepflow");
|
|
18
|
+
|
|
19
|
+
return path.join(configDir, "config.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emptyConfig() {
|
|
23
|
+
return {
|
|
24
|
+
profile: DEFAULT_PROFILE,
|
|
25
|
+
profiles: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readConfig(env = process.env) {
|
|
30
|
+
const configPath = resolveConfigPath(env);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(configPath, "utf8");
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
|
|
36
|
+
if (!parsed || typeof parsed !== "object") {
|
|
37
|
+
throw new Error("config is not an object");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
path: configPath,
|
|
42
|
+
data: {
|
|
43
|
+
...emptyConfig(),
|
|
44
|
+
...parsed,
|
|
45
|
+
profiles:
|
|
46
|
+
parsed.profiles && typeof parsed.profiles === "object"
|
|
47
|
+
? parsed.profiles
|
|
48
|
+
: {},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code === "ENOENT") {
|
|
53
|
+
return {
|
|
54
|
+
path: configPath,
|
|
55
|
+
data: emptyConfig(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new CliError(`Failed to read config: ${error.message}`, {
|
|
60
|
+
code: "config_read_failed",
|
|
61
|
+
exitCode: 1,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function writeConfig(data, env = process.env) {
|
|
67
|
+
const configPath = resolveConfigPath(env);
|
|
68
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
69
|
+
await writeFile(configPath, `${JSON.stringify(data, null, 2)}\n`, {
|
|
70
|
+
mode: 0o600,
|
|
71
|
+
});
|
|
72
|
+
await chmod(configPath, 0o600);
|
|
73
|
+
|
|
74
|
+
return configPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function resolveRuntimeConfig(flags = {}, env = process.env) {
|
|
78
|
+
const config = await readConfig(env);
|
|
79
|
+
const profile = flags.profile || env.DEEPFLOW_PROFILE || config.data.profile || DEFAULT_PROFILE;
|
|
80
|
+
const profileConfig = config.data.profiles?.[profile] ?? {};
|
|
81
|
+
const token = flags.token || env.DEEPFLOW_TOKEN || profileConfig.token || config.data.token || null;
|
|
82
|
+
const baseUrl =
|
|
83
|
+
flags.baseUrl ||
|
|
84
|
+
env.DEEPFLOW_BASE_URL ||
|
|
85
|
+
profileConfig.baseUrl ||
|
|
86
|
+
config.data.baseUrl ||
|
|
87
|
+
DEFAULT_BASE_URL;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
configPath: config.path,
|
|
91
|
+
profile,
|
|
92
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
93
|
+
token,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function saveAuthConfig({
|
|
98
|
+
baseUrl,
|
|
99
|
+
token,
|
|
100
|
+
profile = DEFAULT_PROFILE,
|
|
101
|
+
}, env = process.env) {
|
|
102
|
+
const config = await readConfig(env);
|
|
103
|
+
const next = {
|
|
104
|
+
...config.data,
|
|
105
|
+
profile,
|
|
106
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
107
|
+
token,
|
|
108
|
+
profiles: {
|
|
109
|
+
...(config.data.profiles ?? {}),
|
|
110
|
+
[profile]: {
|
|
111
|
+
...(config.data.profiles?.[profile] ?? {}),
|
|
112
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
113
|
+
token,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const configPath = await writeConfig(next, env);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
configPath,
|
|
122
|
+
data: next,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function clearAuthConfig(profile = DEFAULT_PROFILE, env = process.env) {
|
|
127
|
+
const config = await readConfig(env);
|
|
128
|
+
const nextProfiles = {
|
|
129
|
+
...(config.data.profiles ?? {}),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (nextProfiles[profile]) {
|
|
133
|
+
const { token: _token, ...rest } = nextProfiles[profile];
|
|
134
|
+
nextProfiles[profile] = rest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const next = {
|
|
138
|
+
...config.data,
|
|
139
|
+
profiles: nextProfiles,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if ((next.profile || DEFAULT_PROFILE) === profile) {
|
|
143
|
+
delete next.token;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const configPath = await writeConfig(next, env);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
configPath,
|
|
150
|
+
data: next,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function normalizeBaseUrl(value) {
|
|
155
|
+
return String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
|
|
156
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
constructor(message, {
|
|
3
|
+
code = "error",
|
|
4
|
+
exitCode = 1,
|
|
5
|
+
status = null,
|
|
6
|
+
payload = null,
|
|
7
|
+
} = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CliError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.exitCode = exitCode;
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.payload = payload;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function exitCodeForHttpStatus(status, payload = null) {
|
|
18
|
+
if (status === 401) {
|
|
19
|
+
return 3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (status === 403) {
|
|
23
|
+
return 4;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
status === 502 ||
|
|
28
|
+
payload?.error_type === "proxy_error" ||
|
|
29
|
+
payload?.error_code === "bad_gateway"
|
|
30
|
+
) {
|
|
31
|
+
return 6;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function redactToken(value) {
|
|
38
|
+
if (typeof value !== "string" || !value) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value.length <= 12) {
|
|
43
|
+
return `${value.slice(0, 4)}...`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeError(error) {
|
|
50
|
+
if (error instanceof CliError) {
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new CliError(error instanceof Error ? error.message : String(error));
|
|
55
|
+
}
|
package/src/output.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { normalizeError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function writeJson(io, value, stream = "stdout") {
|
|
4
|
+
io[stream].write(`${JSON.stringify(value, null, 2)}\n`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function writeResult(io, result, { json = false } = {}) {
|
|
8
|
+
if (json) {
|
|
9
|
+
writeJson(io, result);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
switch (result.method) {
|
|
14
|
+
case "auth.login":
|
|
15
|
+
io.stdout.write(`Authenticated: ${result.baseUrl}\n`);
|
|
16
|
+
io.stdout.write(`Config: ${result.configPath}\n`);
|
|
17
|
+
break;
|
|
18
|
+
case "auth.logout":
|
|
19
|
+
io.stdout.write(`Logged out profile "${result.profile}".\n`);
|
|
20
|
+
break;
|
|
21
|
+
case "auth.status":
|
|
22
|
+
io.stdout.write(`Authenticated: ${result.authenticated ? "yes" : "no"}\n`);
|
|
23
|
+
io.stdout.write(`Base URL: ${result.baseUrl}\n`);
|
|
24
|
+
if (result.tokenPrefix) {
|
|
25
|
+
io.stdout.write(`Token: ${result.tokenPrefix}...\n`);
|
|
26
|
+
}
|
|
27
|
+
if (result.status) {
|
|
28
|
+
io.stdout.write(`Status: ${result.status}\n`);
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
case "apps.list":
|
|
32
|
+
if (result.items.length === 0) {
|
|
33
|
+
io.stdout.write("No apps found.\n");
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
for (const app of result.items) {
|
|
37
|
+
io.stdout.write(`${app.id}\t${app.appCode ?? ""}\t${app.name}\t${app.repositoryUrl}\n`);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case "plan.run":
|
|
41
|
+
io.stdout.write("Plan run started.\n");
|
|
42
|
+
io.stdout.write(`Iteration: ${result.iterationId}\n`);
|
|
43
|
+
io.stdout.write(`Backend iteration: ${result.backendIterationId}\n`);
|
|
44
|
+
io.stdout.write(`Plan session: ${result.planSessionId}\n`);
|
|
45
|
+
if (result.runId) {
|
|
46
|
+
io.stdout.write(`Run: ${result.runId}\n`);
|
|
47
|
+
}
|
|
48
|
+
io.stdout.write(`URL: ${result.uiUrl}\n`);
|
|
49
|
+
io.stdout.write(`Status: ${result.statusUrl}\n`);
|
|
50
|
+
io.stdout.write(`Messages: ${result.messagesUrl}\n`);
|
|
51
|
+
io.stdout.write(`Stop: ${result.stopUrl}\n`);
|
|
52
|
+
break;
|
|
53
|
+
case "plan.status":
|
|
54
|
+
io.stdout.write(`Session: ${result.sessionId}\n`);
|
|
55
|
+
io.stdout.write(`Status: ${result.status ?? "unknown"}\n`);
|
|
56
|
+
break;
|
|
57
|
+
case "session.messages":
|
|
58
|
+
io.stdout.write(JSON.stringify(result.messages, null, 2));
|
|
59
|
+
io.stdout.write("\n");
|
|
60
|
+
break;
|
|
61
|
+
case "session.stop":
|
|
62
|
+
io.stdout.write(`Stop requested for session ${result.sessionId}.\n`);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
io.stdout.write(JSON.stringify(result, null, 2));
|
|
66
|
+
io.stdout.write("\n");
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeError(io, error, { json = false } = {}) {
|
|
72
|
+
const normalized = normalizeError(error);
|
|
73
|
+
const payload = {
|
|
74
|
+
ok: false,
|
|
75
|
+
errorCode: normalized.code,
|
|
76
|
+
message: normalized.message,
|
|
77
|
+
status: normalized.status,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (json) {
|
|
81
|
+
writeJson(io, payload, "stderr");
|
|
82
|
+
return normalized.exitCode;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
io.stderr.write(`Error: ${normalized.message}\n`);
|
|
86
|
+
return normalized.exitCode;
|
|
87
|
+
}
|