@enter-pro/enter-cli 0.4.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 +0 -0
- package/dist/auth.d.ts +12 -0
- package/dist/auth.js +39 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +140 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +37 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/domain.d.ts +2 -0
- package/dist/commands/domain.js +65 -0
- package/dist/commands/domain.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +143 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +9 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/models.d.ts +2 -0
- package/dist/commands/models.js +27 -0
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.js +452 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/skill.d.ts +2 -0
- package/dist/commands/skill.js +118 -0
- package/dist/commands/thread.d.ts +2 -0
- package/dist/commands/thread.js +578 -0
- package/dist/commands/thread.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +28 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +178 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +9 -0
- package/dist/lifecycle.js +47 -0
- package/dist/output.d.ts +26 -0
- package/dist/output.js +89 -0
- package/dist/output.js.map +1 -0
- package/dist/poll.d.ts +9 -0
- package/dist/poll.js +24 -0
- package/package.json +41 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
import * as client from "../client.js";
|
|
4
|
+
import { print, printMessage, printResult, printTable, pickList, getFormat } from "../output.js";
|
|
5
|
+
import { pollUntil, TimeoutError } from "../poll.js";
|
|
6
|
+
import { resolveLifecycleStatus } from "../lifecycle.js";
|
|
7
|
+
export const projectCmd = new Command("project")
|
|
8
|
+
.alias("proj")
|
|
9
|
+
.description("Manage projects");
|
|
10
|
+
// `/v1/projects/{id}/detail` sometimes wraps the project under `.project` and
|
|
11
|
+
// sometimes returns the bare project shape — depending on the field. Normalize.
|
|
12
|
+
function unwrapProject(detail) {
|
|
13
|
+
const d = detail;
|
|
14
|
+
return (d.project ?? d);
|
|
15
|
+
}
|
|
16
|
+
function readPublishStatus(project) {
|
|
17
|
+
const ps = project.publish_status;
|
|
18
|
+
const lastPublishedCommit = String(ps?.last_published_commit_id ?? "");
|
|
19
|
+
const lastPublishedAt = String(ps?.last_published_at ?? "");
|
|
20
|
+
const unpublishedChanges = Number(ps?.unpublished_changes ?? 0);
|
|
21
|
+
return {
|
|
22
|
+
lastPublishedCommit,
|
|
23
|
+
lastPublishedAt,
|
|
24
|
+
unpublishedChanges,
|
|
25
|
+
isPublished: Boolean(lastPublishedCommit) && unpublishedChanges === 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
projectCmd
|
|
29
|
+
.command("list <workspace_id>")
|
|
30
|
+
.description("List projects in a workspace")
|
|
31
|
+
.option("--page <number>", "Page number", "1")
|
|
32
|
+
.option("--page-size <number>", "Page size", "20")
|
|
33
|
+
.action(async (id, opts, cmd) => {
|
|
34
|
+
const params = {
|
|
35
|
+
page: opts.page,
|
|
36
|
+
page_size: opts.pageSize,
|
|
37
|
+
};
|
|
38
|
+
const data = await client.get(`/v1/workspaces/${id}/projects`, params);
|
|
39
|
+
const resp = data;
|
|
40
|
+
const items = pickList(resp.projects || [], [
|
|
41
|
+
"project_id", "name", "status", "visibility", "preview_url", "updated_at",
|
|
42
|
+
]);
|
|
43
|
+
const result = { items, total: resp.total, page: resp.page, page_size: resp.page_size };
|
|
44
|
+
const format = getFormat(cmd);
|
|
45
|
+
if (format !== "table") {
|
|
46
|
+
print(format, result);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rows = items.map((p) => [
|
|
50
|
+
String(p.project_id ?? ""),
|
|
51
|
+
String(p.name ?? ""),
|
|
52
|
+
String(p.status ?? ""),
|
|
53
|
+
String(p.visibility ?? ""),
|
|
54
|
+
String(p.updated_at ?? ""),
|
|
55
|
+
]);
|
|
56
|
+
printTable(["Project ID", "Name", "Status", "Visibility", "Updated"], rows);
|
|
57
|
+
});
|
|
58
|
+
projectCmd
|
|
59
|
+
.command("get <project_id>")
|
|
60
|
+
.description("Get project details")
|
|
61
|
+
.action(async (id, _opts, cmd) => {
|
|
62
|
+
const data = await client.get(`/v1/projects/${id}/detail`);
|
|
63
|
+
const p = data;
|
|
64
|
+
const lifecycle = resolveLifecycleStatus(p);
|
|
65
|
+
const enriched = { ...p, lifecycle_status: lifecycle };
|
|
66
|
+
const format = getFormat(cmd);
|
|
67
|
+
if (format !== "table") {
|
|
68
|
+
print(format, enriched);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
printTable(["Project ID", "Name", "Visibility", "Status", "Lifecycle", "Preview URL", "Workspace", "Updated"], [[
|
|
72
|
+
String(p.project_id ?? ""),
|
|
73
|
+
String(p.name ?? ""),
|
|
74
|
+
String(p.visibility ?? ""),
|
|
75
|
+
String(p.status ?? ""),
|
|
76
|
+
lifecycle,
|
|
77
|
+
String(p.preview_url ?? ""),
|
|
78
|
+
String(p.workspace_id ?? ""),
|
|
79
|
+
String(p.updated_at ?? ""),
|
|
80
|
+
]]);
|
|
81
|
+
});
|
|
82
|
+
projectCmd
|
|
83
|
+
.command("create <workspace_id>")
|
|
84
|
+
.description("Create a new project in a workspace")
|
|
85
|
+
.option("--name <name>", "Project name")
|
|
86
|
+
.option("--prompt <text>", "Initial prompt for the project")
|
|
87
|
+
.option("--model <id>", "Pin AI model (see `enter-cli models list`). Invalid IDs silently fall back to 'auto'.")
|
|
88
|
+
.option("--plan-mode", "Start the project in Plan Mode (first turn produces a plan instead of code)")
|
|
89
|
+
.option("--wait", "Wait until the first build completes")
|
|
90
|
+
.option("--timeout <seconds>", "Timeout for --wait in seconds", "300")
|
|
91
|
+
.action(async (id, opts, cmd) => {
|
|
92
|
+
const body = {};
|
|
93
|
+
if (opts.name)
|
|
94
|
+
body.name = opts.name;
|
|
95
|
+
if (opts.prompt)
|
|
96
|
+
body.prompt = opts.prompt;
|
|
97
|
+
if (opts.model)
|
|
98
|
+
body.model_id = opts.model;
|
|
99
|
+
if (opts.planMode)
|
|
100
|
+
body.plan_mode = true;
|
|
101
|
+
const data = await client.post(`/v1/workspaces/${id}/projects`, body);
|
|
102
|
+
if (!opts.wait) {
|
|
103
|
+
print(getFormat(cmd), data);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const created = data;
|
|
107
|
+
const projectId = String(created.project_id ?? created.id ?? "");
|
|
108
|
+
if (!projectId) {
|
|
109
|
+
print(getFormat(cmd), data);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.error("Waiting for first build to complete...");
|
|
113
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
114
|
+
const buildingStatuses = new Set(["initializing", "building"]);
|
|
115
|
+
try {
|
|
116
|
+
const result = await pollUntil(() => client.get(`/v1/projects/${projectId}/detail`), (d) => !buildingStatuses.has(String(d.status ?? "")), {
|
|
117
|
+
intervalMs: 3000,
|
|
118
|
+
timeoutMs,
|
|
119
|
+
onTick: (elapsed) => {
|
|
120
|
+
process.stderr.write(`\rWaiting... ${Math.round(elapsed / 1000)}s elapsed`);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
process.stderr.write("\n");
|
|
124
|
+
const enriched = { ...result, lifecycle_status: resolveLifecycleStatus(result) };
|
|
125
|
+
print(getFormat(cmd), enriched);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err instanceof TimeoutError) {
|
|
129
|
+
console.error(`\nTimed out after ${opts.timeout}s. Project may still be building.`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
projectCmd
|
|
136
|
+
.command("rename <project_id> <new_name>")
|
|
137
|
+
.description("Rename a project")
|
|
138
|
+
.action(async (id, name, _opts, cmd) => {
|
|
139
|
+
await client.post(`/v1/projects/${id}/rename`, { name });
|
|
140
|
+
printResult(getFormat(cmd), { project_id: id, name }, `Project renamed to "${name}".`);
|
|
141
|
+
});
|
|
142
|
+
projectCmd
|
|
143
|
+
.command("delete <project_id>")
|
|
144
|
+
.description("Delete a project")
|
|
145
|
+
.action(async (id, _opts, cmd) => {
|
|
146
|
+
await client.del(`/v1/projects/${id}/delete`);
|
|
147
|
+
printResult(getFormat(cmd), { project_id: id, deleted: true }, "Project deleted successfully.");
|
|
148
|
+
});
|
|
149
|
+
projectCmd
|
|
150
|
+
.command("download <project_id>")
|
|
151
|
+
.description("Download project as zip file")
|
|
152
|
+
.option("--out <path>", "Output file path")
|
|
153
|
+
.action(async (id, opts) => {
|
|
154
|
+
const data = await client.getRaw(`/v1/projects/${id}/download`);
|
|
155
|
+
const outPath = opts.out || `${id}.zip`;
|
|
156
|
+
writeFileSync(outPath, Buffer.from(await data.arrayBuffer()));
|
|
157
|
+
printMessage(`Project downloaded to ${outPath}`);
|
|
158
|
+
});
|
|
159
|
+
projectCmd
|
|
160
|
+
.command("publish <project_id>")
|
|
161
|
+
.description("Publish a project (synchronous: waits for completion and verifies URL)")
|
|
162
|
+
.option("--timeout <seconds>", "Timeout in seconds", "300")
|
|
163
|
+
.action(async (id, opts, cmd) => {
|
|
164
|
+
const initialDetail = await client.get(`/v1/projects/${id}/detail`);
|
|
165
|
+
const initialProject = unwrapProject(initialDetail);
|
|
166
|
+
const buildStatus = initialProject.build_status;
|
|
167
|
+
const commitId = String(buildStatus?.commit_id ?? initialProject.commit ?? "");
|
|
168
|
+
if (!commitId) {
|
|
169
|
+
console.error("Error: project has no committed build yet (build_status.commit_id is empty).");
|
|
170
|
+
console.error("Wait for the latest turn to finish, then retry.");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
await client.post(`/v1/projects/${id}/publish`);
|
|
174
|
+
console.error("Waiting for publish to complete...");
|
|
175
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
176
|
+
try {
|
|
177
|
+
const result = await pollUntil(() => client.get(`/v1/projects/${id}/detail`), (d) => {
|
|
178
|
+
const view = readPublishStatus(unwrapProject(d));
|
|
179
|
+
// Done when the latest committed build has been published and nothing is queued.
|
|
180
|
+
return view.lastPublishedCommit === commitId && view.unpublishedChanges === 0;
|
|
181
|
+
}, {
|
|
182
|
+
intervalMs: 3000,
|
|
183
|
+
timeoutMs,
|
|
184
|
+
onTick: (elapsed) => {
|
|
185
|
+
process.stderr.write(`\rWaiting... ${Math.round(elapsed / 1000)}s elapsed`);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
process.stderr.write("\n");
|
|
189
|
+
const project = unwrapProject(result);
|
|
190
|
+
const view = readPublishStatus(project);
|
|
191
|
+
const publishUrl = String(project.publish_url ?? "");
|
|
192
|
+
const reachable = publishUrl ? await verifyUrlReachable(publishUrl) : false;
|
|
193
|
+
if (publishUrl && !reachable) {
|
|
194
|
+
console.error(`Warning: ${publishUrl} did not return 200 within retries. The publish completed server-side but the URL may still be propagating.`);
|
|
195
|
+
}
|
|
196
|
+
printMessage(`Published commit ${commitId.slice(0, 7)} at ${view.lastPublishedAt}. URL: ${publishUrl}${reachable ? " (200 OK)" : ""}`);
|
|
197
|
+
print(getFormat(cmd), { ...result, lifecycle_status: resolveLifecycleStatus(project) });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof TimeoutError) {
|
|
201
|
+
console.error(`\nTimed out after ${opts.timeout}s. Publish may still be in progress.`);
|
|
202
|
+
console.error(`Run "proj urls ${id}" to check current status.`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// HEAD-first URL probe with a small wall-clock budget. Falls back to GET when
|
|
209
|
+
// HEAD isn't allowed (some hosts return 405). Used by `proj publish` (after
|
|
210
|
+
// publish completes) and `proj urls` (to determine the recommended URL).
|
|
211
|
+
async function verifyUrlReachable(url, attempts = 3, perAttemptMs = 2000) {
|
|
212
|
+
for (let i = 0; i < attempts; i++) {
|
|
213
|
+
const ctrl = new AbortController();
|
|
214
|
+
const t = setTimeout(() => ctrl.abort(), perAttemptMs);
|
|
215
|
+
try {
|
|
216
|
+
let res = await fetch(url, { method: "HEAD", signal: ctrl.signal });
|
|
217
|
+
// Some hosts reject HEAD (405 / 501) — retry once with GET on this attempt.
|
|
218
|
+
if (res.status === 405 || res.status === 501) {
|
|
219
|
+
res = await fetch(url, { method: "GET", signal: ctrl.signal });
|
|
220
|
+
}
|
|
221
|
+
clearTimeout(t);
|
|
222
|
+
if (res.status === 200)
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
clearTimeout(t);
|
|
227
|
+
}
|
|
228
|
+
if (i < attempts - 1)
|
|
229
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
projectCmd
|
|
234
|
+
.command("publish-status <project_id>")
|
|
235
|
+
.description("Get publish status and URLs for a project")
|
|
236
|
+
.action(async (id, _opts, cmd) => {
|
|
237
|
+
const project = unwrapProject(await client.get(`/v1/projects/${id}/detail`));
|
|
238
|
+
const view = readPublishStatus(project);
|
|
239
|
+
const result = {
|
|
240
|
+
status: String(project.status ?? ""),
|
|
241
|
+
lifecycle_status: resolveLifecycleStatus(project),
|
|
242
|
+
published: view.isPublished,
|
|
243
|
+
unpublished_changes: view.unpublishedChanges,
|
|
244
|
+
last_published_commit_id: view.lastPublishedCommit || null,
|
|
245
|
+
last_published_at: view.lastPublishedAt || null,
|
|
246
|
+
publish_url: project.publish_url ?? null,
|
|
247
|
+
preview_url: project.preview_url ?? null,
|
|
248
|
+
publish_commit: project.publish_commit ?? null,
|
|
249
|
+
updated_at: project.updated_at ?? null,
|
|
250
|
+
};
|
|
251
|
+
print(getFormat(cmd), result);
|
|
252
|
+
});
|
|
253
|
+
projectCmd
|
|
254
|
+
.command("urls <project_id>")
|
|
255
|
+
.description("Get all URLs for a project with a recommended share URL")
|
|
256
|
+
.action(async (id, _opts, cmd) => {
|
|
257
|
+
const project = unwrapProject(await client.get(`/v1/projects/${id}/detail`));
|
|
258
|
+
const view = readPublishStatus(project);
|
|
259
|
+
const publishUrl = project.publish_url || "";
|
|
260
|
+
const previewUrl = project.preview_url || "";
|
|
261
|
+
const visibility = project.visibility ?? null;
|
|
262
|
+
const publishCandidate = publishUrl && Boolean(view.lastPublishedCommit);
|
|
263
|
+
// Probe both in parallel — independent network calls.
|
|
264
|
+
const [publishReachable, previewReachable] = await Promise.all([
|
|
265
|
+
publishCandidate ? verifyUrlReachable(publishUrl) : Promise.resolve(false),
|
|
266
|
+
previewUrl ? verifyUrlReachable(previewUrl) : Promise.resolve(false),
|
|
267
|
+
]);
|
|
268
|
+
let recommended = null;
|
|
269
|
+
let environment = "none";
|
|
270
|
+
if (publishReachable) {
|
|
271
|
+
recommended = publishUrl;
|
|
272
|
+
environment = "production";
|
|
273
|
+
}
|
|
274
|
+
else if (previewReachable) {
|
|
275
|
+
recommended = previewUrl;
|
|
276
|
+
environment = "preview";
|
|
277
|
+
}
|
|
278
|
+
const result = {
|
|
279
|
+
preview_url: previewUrl || null,
|
|
280
|
+
preview_reachable: previewReachable,
|
|
281
|
+
publish_url: publishUrl || null,
|
|
282
|
+
publish_reachable: publishReachable,
|
|
283
|
+
last_published_commit_id: view.lastPublishedCommit || null,
|
|
284
|
+
last_published_at: view.lastPublishedAt || null,
|
|
285
|
+
recommended_share_url: recommended,
|
|
286
|
+
environment,
|
|
287
|
+
visibility,
|
|
288
|
+
};
|
|
289
|
+
print(getFormat(cmd), result);
|
|
290
|
+
});
|
|
291
|
+
projectCmd
|
|
292
|
+
.command("remix <project_id>")
|
|
293
|
+
.description("Remix a project")
|
|
294
|
+
.option("--workspace-id <id>", "Target workspace ID for remix")
|
|
295
|
+
.action(async (id, opts, cmd) => {
|
|
296
|
+
const body = {};
|
|
297
|
+
if (opts.workspaceId)
|
|
298
|
+
body.workspace_id = opts.workspaceId;
|
|
299
|
+
const data = await client.post(`/v1/projects/${id}/remix`, body);
|
|
300
|
+
print(getFormat(cmd), data);
|
|
301
|
+
});
|
|
302
|
+
projectCmd
|
|
303
|
+
.command("visibility <project_id> <public|private>")
|
|
304
|
+
.description("Update project visibility")
|
|
305
|
+
.action(async (id, visibility, _opts, cmd) => {
|
|
306
|
+
if (visibility !== "public" && visibility !== "private") {
|
|
307
|
+
throw new Error("Visibility must be 'public' or 'private'");
|
|
308
|
+
}
|
|
309
|
+
await client.post(`/v1/projects/${id}/update_visibility`, { visibility });
|
|
310
|
+
printResult(getFormat(cmd), { project_id: id, visibility }, `Project visibility set to ${visibility}.`);
|
|
311
|
+
});
|
|
312
|
+
projectCmd
|
|
313
|
+
.command("plan-mode <project_id> <enable|disable>")
|
|
314
|
+
.description("Enable or disable plan mode for a project")
|
|
315
|
+
.action(async (id, action, _opts, cmd) => {
|
|
316
|
+
if (action !== "enable" && action !== "disable") {
|
|
317
|
+
throw new Error("Action must be 'enable' or 'disable'");
|
|
318
|
+
}
|
|
319
|
+
const data = await client.post(`/v1/projects/${id}/plan-mode/${action}`);
|
|
320
|
+
const project = unwrapProject(data);
|
|
321
|
+
printResult(getFormat(cmd), project, `Plan mode ${action}d.`);
|
|
322
|
+
});
|
|
323
|
+
projectCmd
|
|
324
|
+
.command("source-code <project_id>")
|
|
325
|
+
.description("Get the full source code of a project")
|
|
326
|
+
.action(async (id, _opts, cmd) => {
|
|
327
|
+
const data = await client.get(`/v1/projects/${id}/source-code`);
|
|
328
|
+
print(getFormat(cmd), data);
|
|
329
|
+
});
|
|
330
|
+
projectCmd
|
|
331
|
+
.command("edit-file <project_id>")
|
|
332
|
+
.description("Directly edit source files in the project (creates a new turn)")
|
|
333
|
+
.requiredOption("--commit <id>", "Current commit ID (from proj get)")
|
|
334
|
+
.requiredOption("--path <file_path>", "File path to edit")
|
|
335
|
+
.requiredOption("--code <content>", "New file content")
|
|
336
|
+
.action(async (id, opts, cmd) => {
|
|
337
|
+
const data = await client.post(`/v1/projects/${id}/edit-code`, {
|
|
338
|
+
commit: opts.commit,
|
|
339
|
+
source_codes: [{ file_path: opts.path, code: opts.code }],
|
|
340
|
+
});
|
|
341
|
+
print(getFormat(cmd), data);
|
|
342
|
+
});
|
|
343
|
+
projectCmd
|
|
344
|
+
.command("model <project_id> <model>")
|
|
345
|
+
.description("Set the AI model for a project (takes effect on next turn)")
|
|
346
|
+
.action(async (id, model, _opts, cmd) => {
|
|
347
|
+
const data = await client.post(`/v1/projects/${id}/model`, { model });
|
|
348
|
+
print(getFormat(cmd), data);
|
|
349
|
+
});
|
|
350
|
+
// MCP subcommand group
|
|
351
|
+
const mcpCmd = new Command("mcp").description("Manage MCP servers for a project");
|
|
352
|
+
mcpCmd
|
|
353
|
+
.command("list <project_id>")
|
|
354
|
+
.description("List MCP servers")
|
|
355
|
+
.action(async (id, _opts, cmd) => {
|
|
356
|
+
const data = await client.get(`/v1/projects/${id}/mcp/servers`);
|
|
357
|
+
const resp = data;
|
|
358
|
+
const format = getFormat(cmd);
|
|
359
|
+
if (format !== "table") {
|
|
360
|
+
print(format, data);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const rows = (resp.servers || []).map((s) => [
|
|
364
|
+
String(s.id ?? ""),
|
|
365
|
+
String(s.name ?? s.display_name ?? ""),
|
|
366
|
+
String(s.transport ?? ""),
|
|
367
|
+
String(s.enabled ?? ""),
|
|
368
|
+
String(s.url ?? ""),
|
|
369
|
+
]);
|
|
370
|
+
printTable(["ID", "Name", "Transport", "Enabled", "URL"], rows);
|
|
371
|
+
});
|
|
372
|
+
mcpCmd
|
|
373
|
+
.command("get <project_id> <server_id>")
|
|
374
|
+
.description("Get an MCP server")
|
|
375
|
+
.action(async (id, serverId, _opts, cmd) => {
|
|
376
|
+
const data = await client.get(`/v1/projects/${id}/mcp/servers/${serverId}`);
|
|
377
|
+
print(getFormat(cmd), data);
|
|
378
|
+
});
|
|
379
|
+
mcpCmd
|
|
380
|
+
.command("create <project_id>")
|
|
381
|
+
.description("Add MCP server(s) to a project")
|
|
382
|
+
.requiredOption("--name <name>", "Server name (key in mcpServers map)")
|
|
383
|
+
.requiredOption("--url <url>", "Server URL")
|
|
384
|
+
.option("--transport <type>", "Transport type: sse, http, stdio")
|
|
385
|
+
.action(async (id, opts, cmd) => {
|
|
386
|
+
const serverConfig = { url: opts.url };
|
|
387
|
+
if (opts.transport)
|
|
388
|
+
serverConfig.transport = opts.transport;
|
|
389
|
+
const data = await client.post(`/v1/projects/${id}/mcp/servers`, {
|
|
390
|
+
mcpServers: { [opts.name]: serverConfig },
|
|
391
|
+
});
|
|
392
|
+
print(getFormat(cmd), data);
|
|
393
|
+
});
|
|
394
|
+
mcpCmd
|
|
395
|
+
.command("update <project_id> <server_id>")
|
|
396
|
+
.description("Update an MCP server")
|
|
397
|
+
.option("--display-name <name>", "New display name")
|
|
398
|
+
.option("--enabled <bool>", "Enable or disable (true/false)")
|
|
399
|
+
.option("--allowed-tools <tools>", "Comma-separated list of allowed tools")
|
|
400
|
+
.action(async (id, serverId, opts, cmd) => {
|
|
401
|
+
const body = {};
|
|
402
|
+
if (opts.displayName)
|
|
403
|
+
body.display_name = opts.displayName;
|
|
404
|
+
if (opts.enabled !== undefined)
|
|
405
|
+
body.enabled = opts.enabled === "true";
|
|
406
|
+
if (opts.allowedTools)
|
|
407
|
+
body.allowed_tools = opts.allowedTools.split(",").map((t) => t.trim());
|
|
408
|
+
const data = await client.patch(`/v1/projects/${id}/mcp/servers/${serverId}`, body);
|
|
409
|
+
print(getFormat(cmd), data ?? { message: "MCP server updated." });
|
|
410
|
+
});
|
|
411
|
+
mcpCmd
|
|
412
|
+
.command("delete <project_id> <server_id>")
|
|
413
|
+
.description("Delete an MCP server")
|
|
414
|
+
.action(async (id, serverId) => {
|
|
415
|
+
await client.del(`/v1/projects/${id}/mcp/servers/${serverId}`);
|
|
416
|
+
printMessage(`MCP server ${serverId} deleted.`);
|
|
417
|
+
});
|
|
418
|
+
projectCmd.addCommand(mcpCmd);
|
|
419
|
+
// Project skills subcommand group
|
|
420
|
+
const projSkillsCmd = new Command("skills").description("Manage project skills");
|
|
421
|
+
projSkillsCmd
|
|
422
|
+
.command("list <project_id>")
|
|
423
|
+
.description("List skills installed on a project")
|
|
424
|
+
.action(async (id, _opts, cmd) => {
|
|
425
|
+
const data = await client.get(`/v1/projects/${id}/skills`);
|
|
426
|
+
const resp = data;
|
|
427
|
+
const format = getFormat(cmd);
|
|
428
|
+
if (format !== "table") {
|
|
429
|
+
print(format, data);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const rows = (resp.skills || []).map((s) => [
|
|
433
|
+
String(s.skill_key ?? ""),
|
|
434
|
+
String(s.name ?? ""),
|
|
435
|
+
String(s.type ?? ""),
|
|
436
|
+
String(s.state ?? ""),
|
|
437
|
+
String(s.version ?? ""),
|
|
438
|
+
]);
|
|
439
|
+
printTable(["Key", "Name", "Type", "State", "Version"], rows);
|
|
440
|
+
});
|
|
441
|
+
projSkillsCmd
|
|
442
|
+
.command("update <project_id>")
|
|
443
|
+
.description("Enable or disable a project skill")
|
|
444
|
+
.requiredOption("--skill-key <key>", "Skill key")
|
|
445
|
+
.requiredOption("--enabled <bool>", "true to enable, false to disable")
|
|
446
|
+
.action(async (id, opts, cmd) => {
|
|
447
|
+
const data = await client.patch(`/v1/projects/${id}/skills`, {
|
|
448
|
+
updates: [{ skill_key: opts.skillKey, enabled: opts.enabled === "true" }],
|
|
449
|
+
});
|
|
450
|
+
print(getFormat(cmd), data);
|
|
451
|
+
});
|
|
452
|
+
projectCmd.addCommand(projSkillsCmd);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as client from "../client.js";
|
|
3
|
+
import { print, printMessage, printTable, pickList } from "../output.js";
|
|
4
|
+
export const skillCmd = new Command("skill").description("Manage skills and agent skill installations");
|
|
5
|
+
function getFormat(cmd) {
|
|
6
|
+
return cmd.optsWithGlobals().output || "json";
|
|
7
|
+
}
|
|
8
|
+
skillCmd
|
|
9
|
+
.command("list")
|
|
10
|
+
.description("List available skills")
|
|
11
|
+
.option("--role <role>", "Filter by minimum role: owner, admin, developer, viewer")
|
|
12
|
+
.action(async (opts, cmd) => {
|
|
13
|
+
const params = {};
|
|
14
|
+
if (opts.role)
|
|
15
|
+
params.role = opts.role;
|
|
16
|
+
const data = await client.workGet("/v1/skills", params);
|
|
17
|
+
const resp = data;
|
|
18
|
+
const items = pickList(resp.skills || [], [
|
|
19
|
+
"slug", "name", "description", "is_public", "latest_version", "published_version", "role",
|
|
20
|
+
]);
|
|
21
|
+
const format = getFormat(cmd);
|
|
22
|
+
if (format !== "table") {
|
|
23
|
+
print(format, { items, total: items.length });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const rows = items.map((s) => [
|
|
27
|
+
String(s.slug ?? ""),
|
|
28
|
+
String(s.name ?? ""),
|
|
29
|
+
String(s.is_public ?? ""),
|
|
30
|
+
String(s.published_version ?? ""),
|
|
31
|
+
String(s.role ?? ""),
|
|
32
|
+
]);
|
|
33
|
+
printTable(["Slug", "Name", "Public", "Version", "Role"], rows);
|
|
34
|
+
});
|
|
35
|
+
skillCmd
|
|
36
|
+
.command("search <keyword>")
|
|
37
|
+
.description("Search skills by keyword")
|
|
38
|
+
.action(async (keyword, _opts, cmd) => {
|
|
39
|
+
const data = await client.workGet("/v1/skills/search", { keyword });
|
|
40
|
+
const resp = data;
|
|
41
|
+
const items = pickList(resp.skills || [], [
|
|
42
|
+
"slug", "name", "description", "is_public", "published_version", "role",
|
|
43
|
+
]);
|
|
44
|
+
const format = getFormat(cmd);
|
|
45
|
+
if (format !== "table") {
|
|
46
|
+
print(format, { items, total: items.length });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rows = items.map((s) => [
|
|
50
|
+
String(s.slug ?? ""),
|
|
51
|
+
String(s.name ?? ""),
|
|
52
|
+
String(s.is_public ?? ""),
|
|
53
|
+
String(s.published_version ?? ""),
|
|
54
|
+
]);
|
|
55
|
+
printTable(["Slug", "Name", "Public", "Version"], rows);
|
|
56
|
+
});
|
|
57
|
+
skillCmd
|
|
58
|
+
.command("get <slug>")
|
|
59
|
+
.description("Get skill details")
|
|
60
|
+
.action(async (slug, _opts, cmd) => {
|
|
61
|
+
const data = await client.workGet(`/v1/skills/${slug}`);
|
|
62
|
+
print(getFormat(cmd), data);
|
|
63
|
+
});
|
|
64
|
+
skillCmd
|
|
65
|
+
.command("delete <slug>")
|
|
66
|
+
.description("Delete a skill")
|
|
67
|
+
.action(async (slug) => {
|
|
68
|
+
await client.workDel(`/v1/skills/${slug}`);
|
|
69
|
+
printMessage(`Skill "${slug}" deleted.`);
|
|
70
|
+
});
|
|
71
|
+
skillCmd
|
|
72
|
+
.command("visibility <slug> <public|private>")
|
|
73
|
+
.description("Set skill visibility")
|
|
74
|
+
.action(async (slug, visibility) => {
|
|
75
|
+
if (visibility !== "public" && visibility !== "private") {
|
|
76
|
+
throw new Error("Visibility must be 'public' or 'private'");
|
|
77
|
+
}
|
|
78
|
+
await client.workPatch(`/v1/skills/${slug}/visibility`, {
|
|
79
|
+
is_public: visibility === "public",
|
|
80
|
+
});
|
|
81
|
+
printMessage(`Skill "${slug}" visibility set to ${visibility}.`);
|
|
82
|
+
});
|
|
83
|
+
skillCmd
|
|
84
|
+
.command("install <slug>")
|
|
85
|
+
.description("Install a skill to an agent")
|
|
86
|
+
.requiredOption("--agent <name>", "Agent name to install the skill to")
|
|
87
|
+
.option("--version <n>", "Skill version to install (default: latest published)")
|
|
88
|
+
.action(async (slug, opts, cmd) => {
|
|
89
|
+
const body = { agent_name: opts.agent };
|
|
90
|
+
if (opts.version)
|
|
91
|
+
body.version = parseInt(opts.version, 10);
|
|
92
|
+
const data = await client.workPost(`/v1/skills/${slug}/install`, body);
|
|
93
|
+
print(getFormat(cmd), data ?? { message: "Skill installed successfully." });
|
|
94
|
+
});
|
|
95
|
+
skillCmd
|
|
96
|
+
.command("uninstall")
|
|
97
|
+
.description("Uninstall a skill from an agent")
|
|
98
|
+
.requiredOption("--agent <name>", "Agent name")
|
|
99
|
+
.requiredOption("--skill <name>", "Skill directory name (from 'skill agents' output)")
|
|
100
|
+
.action(async (opts) => {
|
|
101
|
+
await client.workDel(`/v1/user/agents/${opts.agent}/skills/${opts.skill}`);
|
|
102
|
+
printMessage(`Skill "${opts.skill}" uninstalled from agent "${opts.agent}".`);
|
|
103
|
+
});
|
|
104
|
+
skillCmd
|
|
105
|
+
.command("agents")
|
|
106
|
+
.description("List skills installed on agents")
|
|
107
|
+
.option("--agent <name>", "Show skills for a specific agent only")
|
|
108
|
+
.action(async (opts, cmd) => {
|
|
109
|
+
const format = getFormat(cmd);
|
|
110
|
+
if (opts.agent) {
|
|
111
|
+
const data = await client.workGet(`/v1/user/agents/${opts.agent}/skills`);
|
|
112
|
+
print(format, data);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const data = await client.workGet("/v1/user/agents/skills");
|
|
116
|
+
print(format, data);
|
|
117
|
+
}
|
|
118
|
+
});
|