@enter-pro/enter-cli 0.4.2 → 0.4.3
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 +142 -227
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +114 -6
- package/dist/client.d.ts +1 -4
- package/dist/client.js +13 -63
- package/dist/commands/config.js +5 -10
- package/dist/commands/domain.js +3 -6
- package/dist/commands/login.js +15 -11
- package/dist/commands/logout.js +7 -3
- package/dist/commands/project.js +74 -41
- package/dist/commands/thread.d.ts +27 -0
- package/dist/commands/thread.js +110 -42
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/workspace.js +7 -10
- package/dist/config.d.ts +0 -1
- package/dist/config.js +10 -4
- package/dist/output.d.ts +0 -16
- package/dist/output.js +0 -18
- package/dist/poll.d.ts +1 -0
- package/dist/poll.js +3 -1
- package/dist/thread-events.d.ts +1 -0
- package/dist/thread-events.js +12 -2
- package/dist/workflow.d.ts +44 -0
- package/dist/workflow.js +34 -0
- package/package.json +7 -3
- package/scripts/install-hosts.mjs +29 -0
- package/skills/enter/SKILL.md +36 -0
- package/skills/enter/references/configuration.md +19 -0
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { baseURL, workURL } from "./config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getValidToken } from "./auth.js";
|
|
3
3
|
import { safeOutput } from "./safe-output.js";
|
|
4
4
|
import { APIError, RequestError } from "./errors.js";
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -24,9 +24,6 @@ async function request(method, path, options, base) {
|
|
|
24
24
|
const headers = {
|
|
25
25
|
"Content-Type": "application/json",
|
|
26
26
|
};
|
|
27
|
-
const token = getToken();
|
|
28
|
-
if (token)
|
|
29
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
30
27
|
const init = { method, headers, signal: options?.signal };
|
|
31
28
|
if (options?.body !== undefined) {
|
|
32
29
|
init.body = JSON.stringify(options.body);
|
|
@@ -48,11 +45,17 @@ async function request(method, path, options, base) {
|
|
|
48
45
|
const timer = requestTimeoutMs === undefined ? undefined : setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
49
46
|
init.signal = controller.signal;
|
|
50
47
|
try {
|
|
48
|
+
const token = await getValidToken(controller.signal);
|
|
49
|
+
if (token)
|
|
50
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
51
51
|
for (let attempt = 0;; attempt++) {
|
|
52
52
|
let resp;
|
|
53
53
|
let text;
|
|
54
54
|
try {
|
|
55
55
|
resp = await fetch(url, init);
|
|
56
|
+
if (options?.raw && resp.ok && !resp.headers.get("content-type")?.includes("application/json")) {
|
|
57
|
+
return await resp.arrayBuffer();
|
|
58
|
+
}
|
|
56
59
|
text = await resp.text();
|
|
57
60
|
}
|
|
58
61
|
catch (error) {
|
|
@@ -70,7 +73,7 @@ async function request(method, path, options, base) {
|
|
|
70
73
|
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
71
74
|
}
|
|
72
75
|
if (resp.status === 401) {
|
|
73
|
-
throw new Error("Authentication required. Run `enter login` or set ENTER_API_KEY environment variable.");
|
|
76
|
+
throw new Error("Authentication required. Run `enter-cli login` or set ENTER_API_KEY environment variable.");
|
|
74
77
|
}
|
|
75
78
|
if (retryRead && [502, 503, 504].includes(resp.status) && attempt < 1) {
|
|
76
79
|
await delay(200, undefined, { signal: controller.signal });
|
|
@@ -92,6 +95,8 @@ async function request(method, path, options, base) {
|
|
|
92
95
|
if (resp.status >= 400) {
|
|
93
96
|
throw new RequestError(`HTTP_${resp.status}`, `Enter returned HTTP ${resp.status}.`, readOnly && resp.status >= 500, !readOnly && resp.status >= 500);
|
|
94
97
|
}
|
|
98
|
+
if (options?.raw)
|
|
99
|
+
throw new RequestError("INVALID_RESPONSE", "Expected a binary download, but Enter returned JSON.", true);
|
|
95
100
|
return apiResp;
|
|
96
101
|
}
|
|
97
102
|
if (resp.status >= 500)
|
|
@@ -99,6 +104,8 @@ async function request(method, path, options, base) {
|
|
|
99
104
|
if (apiResp.code !== CODE_SUCCESS) {
|
|
100
105
|
throw new APIError(apiResp.code, apiResp.message ?? "", apiResp.detail ?? "");
|
|
101
106
|
}
|
|
107
|
+
if (options?.raw)
|
|
108
|
+
throw new RequestError("INVALID_RESPONSE", "Expected a binary download, but Enter returned JSON.", true);
|
|
102
109
|
return apiResp.data;
|
|
103
110
|
}
|
|
104
111
|
}
|
|
@@ -122,9 +129,6 @@ export async function post(path, body, signal) {
|
|
|
122
129
|
export async function del(path) {
|
|
123
130
|
return request("DELETE", path);
|
|
124
131
|
}
|
|
125
|
-
export async function put(path, body) {
|
|
126
|
-
return request("PUT", path, { body });
|
|
127
|
-
}
|
|
128
132
|
export async function patch(path, body) {
|
|
129
133
|
return request("PATCH", path, { body });
|
|
130
134
|
}
|
|
@@ -135,60 +139,6 @@ export async function workGet(path, params) {
|
|
|
135
139
|
export async function workPost(path, body) {
|
|
136
140
|
return request("POST", path, { body }, workURL());
|
|
137
141
|
}
|
|
138
|
-
export async function workPatch(path, body) {
|
|
139
|
-
return request("PATCH", path, { body }, workURL());
|
|
140
|
-
}
|
|
141
|
-
export async function workDel(path) {
|
|
142
|
-
return request("DELETE", path, {}, workURL());
|
|
143
|
-
}
|
|
144
142
|
export async function getRaw(path) {
|
|
145
|
-
|
|
146
|
-
const headers = {};
|
|
147
|
-
const token = getToken();
|
|
148
|
-
if (token)
|
|
149
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
150
|
-
if (verbose)
|
|
151
|
-
console.error(`> GET ${url}`);
|
|
152
|
-
const resp = await fetch(url, { headers });
|
|
153
|
-
if (verbose)
|
|
154
|
-
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
155
|
-
if (resp.status === 401) {
|
|
156
|
-
throw new Error("Authentication required. Run `enter login` or set ENTER_API_KEY environment variable.");
|
|
157
|
-
}
|
|
158
|
-
// Successful binary response — caller consumes the body.
|
|
159
|
-
if (resp.ok) {
|
|
160
|
-
// But: a JSON error envelope can come back with status 200 too. Peek the
|
|
161
|
-
// content-type; only treat as raw if it's not JSON.
|
|
162
|
-
const ct = resp.headers.get("content-type") ?? "";
|
|
163
|
-
if (!ct.includes("application/json"))
|
|
164
|
-
return resp;
|
|
165
|
-
const text = await resp.text();
|
|
166
|
-
try {
|
|
167
|
-
const apiResp = JSON.parse(text);
|
|
168
|
-
if (apiResp.code !== 0) {
|
|
169
|
-
throw new APIError(apiResp.code, apiResp.message, apiResp.detail);
|
|
170
|
-
}
|
|
171
|
-
// code === 0 with JSON body on a binary endpoint shouldn't happen; surface as raw text
|
|
172
|
-
throw new Error(`Unexpected JSON response on binary endpoint: ${text.slice(0, 200)}`);
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
if (err instanceof APIError)
|
|
176
|
-
throw err;
|
|
177
|
-
throw new Error(`Failed to parse JSON response: ${text.slice(0, 200)}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// Non-2xx — try to parse the API error envelope so callers can distinguish
|
|
181
|
-
// VIP_REQUIRED, NOT_FOUND, etc. Fall back to raw text if it isn't JSON.
|
|
182
|
-
const text = await resp.text();
|
|
183
|
-
try {
|
|
184
|
-
const apiResp = JSON.parse(text);
|
|
185
|
-
if (apiResp.code !== undefined) {
|
|
186
|
-
throw new APIError(apiResp.code, apiResp.message ?? "", apiResp.detail ?? "");
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
if (err instanceof APIError)
|
|
191
|
-
throw err;
|
|
192
|
-
}
|
|
193
|
-
throw new Error(`HTTP ${resp.status}: ${text || resp.statusText}`);
|
|
143
|
+
return request("GET", path, { raw: true });
|
|
194
144
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { setConfig, getConfig, allSettings } from "../config.js";
|
|
3
|
-
import { print,
|
|
3
|
+
import { print, printResult, printTable, getFormat } from "../output.js";
|
|
4
4
|
export const configCmd = new Command("config").description("Manage CLI configuration");
|
|
5
5
|
configCmd
|
|
6
6
|
.command("set <key> <value>")
|
|
7
7
|
.description("Set a configuration value")
|
|
8
|
-
.action(async (key, value) => {
|
|
8
|
+
.action(async (key, value, _opts, cmd) => {
|
|
9
9
|
setConfig(key, value);
|
|
10
|
-
|
|
10
|
+
printResult(getFormat(cmd), { key, value }, `Set ${key} = ${value}`);
|
|
11
11
|
});
|
|
12
12
|
configCmd
|
|
13
13
|
.command("get <key>")
|
|
14
14
|
.description("Get a configuration value")
|
|
15
|
-
.action(async (key) => {
|
|
15
|
+
.action(async (key, _opts, cmd) => {
|
|
16
16
|
const val = getConfig(key);
|
|
17
|
-
|
|
18
|
-
printMessage(`${key}: (not set)`);
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
printMessage(`${key}: ${val}`);
|
|
22
|
-
}
|
|
17
|
+
printResult(getFormat(cmd), { key, value: val || null }, val ? `${key}: ${val}` : `${key}: (not set)`);
|
|
23
18
|
});
|
|
24
19
|
configCmd
|
|
25
20
|
.command("list")
|
package/dist/commands/domain.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import * as client from "../client.js";
|
|
3
|
-
import { print,
|
|
3
|
+
import { print, printResult, printTable, getFormat } from "../output.js";
|
|
4
4
|
export const domainCmd = new Command("domain").description("Manage project domains");
|
|
5
|
-
function getFormat(cmd) {
|
|
6
|
-
return cmd.optsWithGlobals().output || "json";
|
|
7
|
-
}
|
|
8
5
|
domainCmd
|
|
9
6
|
.command("list <project_id>")
|
|
10
7
|
.description("List project domains")
|
|
@@ -37,9 +34,9 @@ domainCmd
|
|
|
37
34
|
.command("remove <project_id>")
|
|
38
35
|
.description("Remove a custom domain from project")
|
|
39
36
|
.requiredOption("--domain <name>", "Domain name to remove")
|
|
40
|
-
.action(async (id, opts) => {
|
|
37
|
+
.action(async (id, opts, cmd) => {
|
|
41
38
|
await client.del(`/v1/projects/${id}/domain?domain=${opts.domain}`);
|
|
42
|
-
|
|
39
|
+
printResult(getFormat(cmd), { removed: true, domain: opts.domain }, `Domain "${opts.domain}" removed.`);
|
|
43
40
|
});
|
|
44
41
|
domainCmd
|
|
45
42
|
.command("refresh <project_id>")
|
package/dist/commands/login.js
CHANGED
|
@@ -2,12 +2,12 @@ import { Command } from "commander";
|
|
|
2
2
|
import { createServer } from "http";
|
|
3
3
|
import { randomBytes, createHash } from "crypto";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
|
-
import { saveCredentials, verifyAccessToken } from "../auth.js";
|
|
6
|
-
import {
|
|
5
|
+
import { saveCredentials, verifyAccessToken, OAUTH_TOKEN_URL, OAUTH_CLIENT_ID } from "../auth.js";
|
|
6
|
+
import { printResult, getFormat } from "../output.js";
|
|
7
7
|
const AUTH0_DOMAIN = "auth.converge.ai";
|
|
8
|
-
const CLIENT_ID =
|
|
8
|
+
const CLIENT_ID = OAUTH_CLIENT_ID;
|
|
9
9
|
const AUTHORIZE_URL = `https://${AUTH0_DOMAIN}/authorize`;
|
|
10
|
-
const TOKEN_URL =
|
|
10
|
+
const TOKEN_URL = OAUTH_TOKEN_URL;
|
|
11
11
|
const AUDIENCE = "https://api.enter.pro";
|
|
12
12
|
const SCOPE = "openid profile email offline_access";
|
|
13
13
|
const REDIRECT_URI = "http://localhost:19820";
|
|
@@ -94,7 +94,6 @@ function startOAuthFlow() {
|
|
|
94
94
|
});
|
|
95
95
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
96
96
|
res.end(`<html><body><h2>Login Successful!</h2><p>You can close this window and return to the terminal.</p></body></html>`);
|
|
97
|
-
printMessage("Login successful. Credentials saved.");
|
|
98
97
|
clearTimeout(timeoutId);
|
|
99
98
|
server.close();
|
|
100
99
|
resolve();
|
|
@@ -118,8 +117,8 @@ function startOAuthFlow() {
|
|
|
118
117
|
code_challenge_method: "S256",
|
|
119
118
|
});
|
|
120
119
|
const authUrl = `${AUTHORIZE_URL}?${params}`;
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
console.error("Opening browser for authentication...");
|
|
121
|
+
console.error(`If browser doesn't open, visit:\n${authUrl}\n`);
|
|
123
122
|
openBrowser(authUrl);
|
|
124
123
|
});
|
|
125
124
|
timeoutId = setTimeout(() => {
|
|
@@ -131,15 +130,20 @@ function startOAuthFlow() {
|
|
|
131
130
|
export const loginCmd = new Command("login")
|
|
132
131
|
.description("Authenticate with Enter platform")
|
|
133
132
|
.option("--api-key <key>", "Authenticate with a workspace API key instead of OAuth")
|
|
134
|
-
.action(async (opts) => {
|
|
133
|
+
.action(async (opts, cmd) => {
|
|
135
134
|
if (opts.apiKey) {
|
|
136
135
|
await verifyAccessToken(opts.apiKey);
|
|
137
136
|
saveCredentials({
|
|
138
137
|
access_token: opts.apiKey,
|
|
139
138
|
token_type: "Bearer",
|
|
140
139
|
});
|
|
141
|
-
printMessage("API key saved. You are now authenticated.");
|
|
142
|
-
return;
|
|
143
140
|
}
|
|
144
|
-
|
|
141
|
+
else {
|
|
142
|
+
await startOAuthFlow();
|
|
143
|
+
}
|
|
144
|
+
const injected = Boolean(process.env.ENTER_API_KEY);
|
|
145
|
+
const message = injected
|
|
146
|
+
? "Local credentials verified and saved. ENTER_API_KEY still takes precedence; this has not switched the active account. Change authentication in the host or unset ENTER_API_KEY to use the saved credentials."
|
|
147
|
+
: "Login successful. Credentials saved.";
|
|
148
|
+
printResult(getFormat(cmd), { credentials_saved: true, auth_source: injected ? "environment" : "local", ...(!injected ? { authenticated: true } : {}), message }, message);
|
|
145
149
|
});
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { clearCredentials } from "../auth.js";
|
|
3
|
-
import {
|
|
3
|
+
import { printResult, getFormat } from "../output.js";
|
|
4
4
|
export const logoutCmd = new Command("logout")
|
|
5
5
|
.description("Clear stored credentials")
|
|
6
|
-
.action(async () => {
|
|
6
|
+
.action(async (_opts, cmd) => {
|
|
7
7
|
clearCredentials();
|
|
8
|
-
|
|
8
|
+
const injected = Boolean(process.env.ENTER_API_KEY);
|
|
9
|
+
const message = injected
|
|
10
|
+
? "Local credentials cleared. ENTER_API_KEY still takes precedence; change or disconnect authentication in the host, or unset it in your shell, to switch accounts."
|
|
11
|
+
: "Stored credentials cleared. Logged out.";
|
|
12
|
+
printResult(getFormat(cmd), { logged_out: !injected, local_credentials_cleared: true, auth_source: injected ? "environment" : "none", message }, message);
|
|
9
13
|
});
|
package/dist/commands/project.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { writeFileSync } from "fs";
|
|
3
4
|
import * as client from "../client.js";
|
|
@@ -5,6 +6,7 @@ import { print, printMessage, printResult, printTable, pickList, getFormat } fro
|
|
|
5
6
|
import { pollUntil, TimeoutError } from "../poll.js";
|
|
6
7
|
import { reportThread, threadInteraction } from "./thread.js";
|
|
7
8
|
import { safeOutput } from "../safe-output.js";
|
|
9
|
+
import { RequestError } from "../errors.js";
|
|
8
10
|
import { resolveLifecycleStatus } from "../lifecycle.js";
|
|
9
11
|
export const projectCmd = new Command("project")
|
|
10
12
|
.alias("proj")
|
|
@@ -63,7 +65,8 @@ projectCmd
|
|
|
63
65
|
.action(async (id, _opts, cmd) => {
|
|
64
66
|
const data = await client.get(`/v1/projects/${id}/detail`);
|
|
65
67
|
const p = data;
|
|
66
|
-
const
|
|
68
|
+
const project = unwrapProject(p);
|
|
69
|
+
const lifecycle = resolveLifecycleStatus(project);
|
|
67
70
|
const enriched = { ...p, lifecycle_status: lifecycle };
|
|
68
71
|
const format = getFormat(cmd);
|
|
69
72
|
if (format !== "table") {
|
|
@@ -71,14 +74,14 @@ projectCmd
|
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
printTable(["Project ID", "Name", "Visibility", "Status", "Lifecycle", "Preview URL", "Workspace", "Updated"], [[
|
|
74
|
-
String(
|
|
75
|
-
String(
|
|
76
|
-
String(
|
|
77
|
-
String(
|
|
77
|
+
String(project.project_id ?? ""),
|
|
78
|
+
String(project.name ?? ""),
|
|
79
|
+
String(project.visibility ?? ""),
|
|
80
|
+
String(project.status ?? ""),
|
|
78
81
|
lifecycle,
|
|
79
|
-
String(
|
|
80
|
-
String(
|
|
81
|
-
String(
|
|
82
|
+
String(project.preview_url ?? ""),
|
|
83
|
+
String(project.workspace_id ?? ""),
|
|
84
|
+
String(project.updated_at ?? ""),
|
|
82
85
|
]]);
|
|
83
86
|
});
|
|
84
87
|
projectCmd
|
|
@@ -132,83 +135,108 @@ projectCmd
|
|
|
132
135
|
.command("download <project_id>")
|
|
133
136
|
.description("Download project as zip file")
|
|
134
137
|
.option("--out <path>", "Output file path")
|
|
135
|
-
.action(async (id, opts) => {
|
|
138
|
+
.action(async (id, opts, cmd) => {
|
|
136
139
|
const data = await client.getRaw(`/v1/projects/${id}/download`);
|
|
137
140
|
const outPath = opts.out || `${id}.zip`;
|
|
138
|
-
writeFileSync(outPath, Buffer.from(
|
|
139
|
-
|
|
141
|
+
writeFileSync(outPath, Buffer.from(data));
|
|
142
|
+
printResult(getFormat(cmd), { path: outPath }, `Project downloaded to ${outPath}`);
|
|
140
143
|
});
|
|
141
144
|
projectCmd
|
|
142
145
|
.command("publish <project_id>")
|
|
143
146
|
.description("Publish a project (synchronous: waits for completion and verifies URL)")
|
|
144
147
|
.option("--timeout <seconds>", "Timeout in seconds", "300")
|
|
145
148
|
.action(async (id, opts, cmd) => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
await client.post(`/v1/projects/${id}/publish`);
|
|
156
|
-
console.error("Waiting for publish to complete...");
|
|
157
|
-
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
149
|
+
const seconds = Number(opts.timeout);
|
|
150
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds * 1000 > 2147483647)
|
|
151
|
+
throw new Error("--timeout must be a positive bounded number");
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const timer = setTimeout(() => controller.abort(), seconds * 1000);
|
|
154
|
+
let submitted = false;
|
|
155
|
+
let published = false;
|
|
158
156
|
try {
|
|
159
|
-
const
|
|
157
|
+
const initialDetail = await client.get(`/v1/projects/${id}/detail`, undefined, controller.signal);
|
|
158
|
+
const initialProject = unwrapProject(initialDetail);
|
|
159
|
+
const buildStatus = initialProject.build_status;
|
|
160
|
+
const commitId = String(buildStatus?.commit_id ?? initialProject.commit ?? "");
|
|
161
|
+
if (!commitId) {
|
|
162
|
+
throw new Error("Project has no committed build yet. Wait for the latest turn to finish, then retry.");
|
|
163
|
+
}
|
|
164
|
+
const format = getFormat(cmd);
|
|
165
|
+
const human = format === "table";
|
|
166
|
+
controller.signal.throwIfAborted();
|
|
167
|
+
submitted = true;
|
|
168
|
+
await client.post(`/v1/projects/${id}/publish`, undefined, controller.signal);
|
|
169
|
+
if (human)
|
|
170
|
+
console.error("Waiting for publish to complete...");
|
|
171
|
+
const timeoutMs = seconds * 1000;
|
|
172
|
+
const result = await pollUntil(() => client.get(`/v1/projects/${id}/detail`, undefined, controller.signal), (d) => {
|
|
160
173
|
const view = readPublishStatus(unwrapProject(d));
|
|
161
174
|
// Done when the latest committed build has been published and nothing is queued.
|
|
162
175
|
return view.lastPublishedCommit === commitId && view.unpublishedChanges === 0;
|
|
163
176
|
}, {
|
|
177
|
+
signal: controller.signal,
|
|
164
178
|
intervalMs: 3000,
|
|
165
179
|
timeoutMs,
|
|
166
180
|
onTick: (elapsed) => {
|
|
167
|
-
|
|
181
|
+
if (human)
|
|
182
|
+
process.stderr.write(`\rWaiting... ${Math.round(elapsed / 1000)}s elapsed`);
|
|
168
183
|
},
|
|
169
184
|
});
|
|
170
|
-
|
|
185
|
+
if (human)
|
|
186
|
+
process.stderr.write("\n");
|
|
187
|
+
published = true;
|
|
171
188
|
const project = unwrapProject(result);
|
|
172
189
|
const view = readPublishStatus(project);
|
|
173
190
|
const publishUrl = String(project.publish_url ?? "");
|
|
174
|
-
const reachable = publishUrl ? await verifyUrlReachable(publishUrl) : false;
|
|
175
|
-
if (publishUrl && !reachable) {
|
|
191
|
+
const reachable = publishUrl ? await verifyUrlReachable(publishUrl, 3, 2000, controller.signal) : false;
|
|
192
|
+
if (human && publishUrl && !reachable) {
|
|
176
193
|
console.error(`Warning: ${publishUrl} did not return 200 within retries. The publish completed server-side but the URL may still be propagating.`);
|
|
177
194
|
}
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
if (human)
|
|
196
|
+
printMessage(`Published commit ${commitId.slice(0, 7)} at ${view.lastPublishedAt}. URL: ${publishUrl}${reachable ? " (200 OK)" : ""}`);
|
|
197
|
+
print(format, { ...result, lifecycle_status: resolveLifecycleStatus(project), publish_reachable: reachable });
|
|
180
198
|
}
|
|
181
199
|
catch (err) {
|
|
182
|
-
if (err instanceof TimeoutError) {
|
|
183
|
-
|
|
184
|
-
console.error(`Run "proj urls ${id}" to check current status.`);
|
|
185
|
-
process.exit(1);
|
|
200
|
+
if (err instanceof TimeoutError || controller.signal.aborted) {
|
|
201
|
+
throw new RequestError("PUBLISH_TIMEOUT", `Publish observation exceeded its deadline. Check enter-cli project publish-status ${id} before retrying.`, false, submitted && !published);
|
|
186
202
|
}
|
|
187
203
|
throw err;
|
|
188
204
|
}
|
|
205
|
+
finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
189
208
|
});
|
|
190
209
|
// HEAD-first URL probe with a small wall-clock budget. Falls back to GET when
|
|
191
210
|
// HEAD isn't allowed (some hosts return 405). Used by `proj publish` (after
|
|
192
211
|
// publish completes) and `proj urls` (to determine the recommended URL).
|
|
193
|
-
async function verifyUrlReachable(url, attempts = 3, perAttemptMs = 2000) {
|
|
212
|
+
async function verifyUrlReachable(url, attempts = 3, perAttemptMs = 2000, signal) {
|
|
194
213
|
for (let i = 0; i < attempts; i++) {
|
|
214
|
+
signal?.throwIfAborted();
|
|
195
215
|
const ctrl = new AbortController();
|
|
216
|
+
const abort = () => ctrl.abort();
|
|
217
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
196
218
|
const t = setTimeout(() => ctrl.abort(), perAttemptMs);
|
|
197
219
|
try {
|
|
198
220
|
let res = await fetch(url, { method: "HEAD", signal: ctrl.signal });
|
|
199
221
|
// Some hosts reject HEAD (405 / 501) — retry once with GET on this attempt.
|
|
200
222
|
if (res.status === 405 || res.status === 501) {
|
|
223
|
+
await res.body?.cancel();
|
|
201
224
|
res = await fetch(url, { method: "GET", signal: ctrl.signal });
|
|
202
225
|
}
|
|
203
|
-
|
|
226
|
+
await res.body?.cancel();
|
|
204
227
|
if (res.status === 200)
|
|
205
228
|
return true;
|
|
206
229
|
}
|
|
207
230
|
catch {
|
|
231
|
+
signal?.throwIfAborted();
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
ctrl.abort();
|
|
208
235
|
clearTimeout(t);
|
|
236
|
+
signal?.removeEventListener("abort", abort);
|
|
209
237
|
}
|
|
210
238
|
if (i < attempts - 1)
|
|
211
|
-
await
|
|
239
|
+
await delay(1000, undefined, { signal });
|
|
212
240
|
}
|
|
213
241
|
return false;
|
|
214
242
|
}
|
|
@@ -276,8 +304,13 @@ projectCmd
|
|
|
276
304
|
.option("--workspace-id <id>", "Target workspace ID for remix")
|
|
277
305
|
.action(async (id, opts, cmd) => {
|
|
278
306
|
const body = {};
|
|
279
|
-
if (opts.workspaceId)
|
|
280
|
-
|
|
307
|
+
if (opts.workspaceId !== undefined) {
|
|
308
|
+
const workspaceId = Number(opts.workspaceId);
|
|
309
|
+
if (!/^\d+$/.test(opts.workspaceId) || !Number.isSafeInteger(workspaceId) || workspaceId <= 0) {
|
|
310
|
+
throw new Error("--workspace-id must be a positive safe integer");
|
|
311
|
+
}
|
|
312
|
+
body.workspace_id = workspaceId;
|
|
313
|
+
}
|
|
281
314
|
const data = await client.post(`/v1/projects/${id}/remix`, body);
|
|
282
315
|
print(getFormat(cmd), data);
|
|
283
316
|
});
|
|
@@ -393,9 +426,9 @@ mcpCmd
|
|
|
393
426
|
mcpCmd
|
|
394
427
|
.command("delete <project_id> <server_id>")
|
|
395
428
|
.description("Delete an MCP server")
|
|
396
|
-
.action(async (id, serverId) => {
|
|
429
|
+
.action(async (id, serverId, _opts, cmd) => {
|
|
397
430
|
await client.del(`/v1/projects/${id}/mcp/servers/${serverId}`);
|
|
398
|
-
|
|
431
|
+
printResult(getFormat(cmd), { deleted: true, server_id: serverId }, `MCP server ${serverId} deleted.`);
|
|
399
432
|
});
|
|
400
433
|
projectCmd.addCommand(mcpCmd);
|
|
401
434
|
// Project skills subcommand group
|
|
@@ -7,6 +7,33 @@ type InteractionOptions = {
|
|
|
7
7
|
requireBuild?: boolean;
|
|
8
8
|
};
|
|
9
9
|
export declare function threadInteraction(id: string, { taskId, chatId, turn, requireBuild }?: InteractionOptions): {
|
|
10
|
+
workflow: {
|
|
11
|
+
actions?: {
|
|
12
|
+
id: unknown;
|
|
13
|
+
kind: unknown;
|
|
14
|
+
decision: string;
|
|
15
|
+
required_fields: {};
|
|
16
|
+
submit_command: unknown;
|
|
17
|
+
}[] | undefined;
|
|
18
|
+
build?: {
|
|
19
|
+
commit: {} | null;
|
|
20
|
+
matches_task: boolean;
|
|
21
|
+
success: {} | null;
|
|
22
|
+
} | undefined;
|
|
23
|
+
observation: {
|
|
24
|
+
timed_out: boolean;
|
|
25
|
+
retrying: boolean;
|
|
26
|
+
interrupted: boolean;
|
|
27
|
+
};
|
|
28
|
+
reason?: string | undefined;
|
|
29
|
+
state: string;
|
|
30
|
+
task: {
|
|
31
|
+
id: {} | null;
|
|
32
|
+
turn: {} | null;
|
|
33
|
+
chat_id: {} | null;
|
|
34
|
+
};
|
|
35
|
+
next_action: string;
|
|
36
|
+
};
|
|
10
37
|
monitoring_required: boolean;
|
|
11
38
|
wait_command: string;
|
|
12
39
|
watch_command: string;
|