@allocator-one/harness-cli 1.0.0 → 1.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
CHANGED
|
@@ -56,7 +56,7 @@ tool — issue_list, issue_show, issue_create, issue_update, issue_claim,
|
|
|
56
56
|
issue_close, issue_comment, relation_add/rm, link_list/add/rm,
|
|
57
57
|
project_list/show/create/update, milestone_list/create,
|
|
58
58
|
document_list/show/create/update, label_list/create, team_list,
|
|
59
|
-
workflow_states, user_list, stats, upload. Read-only tools carry
|
|
59
|
+
team_create, workflow_states, user_list, stats, upload. Read-only tools carry
|
|
60
60
|
`readOnlyHint`. Results are compact JSON of the data envelope.
|
|
61
61
|
|
|
62
62
|
Register it with Claude Code:
|
|
@@ -108,7 +108,11 @@ var Client = class {
|
|
|
108
108
|
async getBytes(path2) {
|
|
109
109
|
const response = await this.raw("GET", path2, {});
|
|
110
110
|
if (!response.ok) throw await this.toHttpError(response);
|
|
111
|
-
|
|
111
|
+
try {
|
|
112
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new TransportError(transportMessage(error));
|
|
115
|
+
}
|
|
112
116
|
}
|
|
113
117
|
/**
|
|
114
118
|
* The server's API version, for `version`'s server side. The header rides
|
|
@@ -155,7 +159,12 @@ var Client = class {
|
|
|
155
159
|
}
|
|
156
160
|
async json(response) {
|
|
157
161
|
if (!response.ok) throw await this.toHttpError(response);
|
|
158
|
-
|
|
162
|
+
let text;
|
|
163
|
+
try {
|
|
164
|
+
text = await response.text();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new TransportError(transportMessage(error));
|
|
167
|
+
}
|
|
159
168
|
if (text === "") return { data: void 0 };
|
|
160
169
|
try {
|
|
161
170
|
return JSON.parse(text);
|
|
@@ -505,7 +514,7 @@ function pageCount(meta) {
|
|
|
505
514
|
function dedupe(records) {
|
|
506
515
|
const seen = /* @__PURE__ */ new Set();
|
|
507
516
|
return records.filter((record) => {
|
|
508
|
-
const key = record["id"] ?? record;
|
|
517
|
+
const key = record["id"] ?? JSON.stringify(record);
|
|
509
518
|
if (seen.has(key)) return false;
|
|
510
519
|
seen.add(key);
|
|
511
520
|
return true;
|
|
@@ -1016,7 +1025,7 @@ function str2(value) {
|
|
|
1016
1025
|
}
|
|
1017
1026
|
|
|
1018
1027
|
// src/commands/extended.ts
|
|
1019
|
-
var SAFE_ID = /^[A-Za-z0-9._-]+$/;
|
|
1028
|
+
var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
1020
1029
|
function requireSafeId(kind, id) {
|
|
1021
1030
|
if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
|
|
1022
1031
|
}
|
|
@@ -1219,7 +1228,7 @@ async function linkRm(ctx, issue, idOrUrl) {
|
|
|
1219
1228
|
const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
|
|
1220
1229
|
const link = body.data.find((candidate) => candidate["id"] === idOrUrl || candidate["url"] === idOrUrl);
|
|
1221
1230
|
if (link === void 0) {
|
|
1222
|
-
throw new
|
|
1231
|
+
throw new CommandError(`no link on ${issue} matches ${idOrUrl} (by id or exact URL)`);
|
|
1223
1232
|
}
|
|
1224
1233
|
await ctx.client.delete(`/api/v1/issues/${issue}/links/${str3(link["id"])}`);
|
|
1225
1234
|
return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
|
|
@@ -1241,6 +1250,24 @@ async function labelCreate(ctx, name, color) {
|
|
|
1241
1250
|
return { json: response.data, human: `Created label ${clean(response.data["name"])}
|
|
1242
1251
|
` };
|
|
1243
1252
|
}
|
|
1253
|
+
var TEAM_CREATE_KEY = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
1254
|
+
async function teamCreate(ctx, opts) {
|
|
1255
|
+
if (opts.key === void 0 || opts.name === void 0) {
|
|
1256
|
+
throw new UsageError("usage: harness team create --key <key> --name <name>");
|
|
1257
|
+
}
|
|
1258
|
+
if (!TEAM_CREATE_KEY.test(opts.key)) {
|
|
1259
|
+
throw new UsageError(`not a team key: ${opts.key} (expected e.g. ALL \u2014 letters and digits, letter first)`);
|
|
1260
|
+
}
|
|
1261
|
+
const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
|
|
1262
|
+
const team = created.data;
|
|
1263
|
+
const states = await ctx.client.get(`/api/v1/teams/${str3(team["key"])}/workflow_states`);
|
|
1264
|
+
const human = `Created team ${str3(team["key"])} \u2014 ${clean(team["name"])}
|
|
1265
|
+
` + table(
|
|
1266
|
+
["NAME", "TYPE"],
|
|
1267
|
+
states.data.map((state) => [str3(state["name"]), str3(state["type"])])
|
|
1268
|
+
);
|
|
1269
|
+
return { json: { ...team, workflow_states: states.data }, human };
|
|
1270
|
+
}
|
|
1244
1271
|
async function teamList(ctx) {
|
|
1245
1272
|
const body = await ctx.client.get("/api/v1/teams");
|
|
1246
1273
|
const human = body.data.length === 0 ? "No teams.\n" : table(
|
|
@@ -1371,6 +1398,9 @@ function validateBaseUrl(baseUrl) {
|
|
|
1371
1398
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
1372
1399
|
return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
|
|
1373
1400
|
}
|
|
1401
|
+
if (parsed.search !== "" || parsed.hash !== "") {
|
|
1402
|
+
return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
|
|
1403
|
+
}
|
|
1374
1404
|
if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
|
|
1375
1405
|
if (parsed.protocol === "http:") {
|
|
1376
1406
|
if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
|
|
@@ -1444,6 +1474,7 @@ export {
|
|
|
1444
1474
|
linkRm,
|
|
1445
1475
|
labelList,
|
|
1446
1476
|
labelCreate,
|
|
1477
|
+
teamCreate,
|
|
1447
1478
|
teamList,
|
|
1448
1479
|
workflowStates,
|
|
1449
1480
|
userList,
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
search,
|
|
46
46
|
show,
|
|
47
47
|
stats,
|
|
48
|
+
teamCreate,
|
|
48
49
|
teamList,
|
|
49
50
|
update,
|
|
50
51
|
upload,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
version,
|
|
54
55
|
workflowStates,
|
|
55
56
|
writeCredentials
|
|
56
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-JOT3TGQQ.js";
|
|
57
58
|
|
|
58
59
|
// src/cli.ts
|
|
59
60
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
@@ -640,6 +641,9 @@ var team = program.command("team").description("teams");
|
|
|
640
641
|
team.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
641
642
|
await withConfig(opts.json === true, false, (ctx) => teamList(ctx));
|
|
642
643
|
});
|
|
644
|
+
team.command("create").description("create a team, seeded with the default workflow states").option("--key <key>", "identifier prefix for the team's issues, e.g. IOS (required)").option("--name <name>", "display name (required)").option("--json", "emit JSON").action(async (opts) => {
|
|
645
|
+
await withConfig(opts.json === true, true, (ctx) => teamCreate(ctx, opts));
|
|
646
|
+
});
|
|
643
647
|
program.command("states").description("a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
|
|
644
648
|
await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
|
|
645
649
|
});
|
|
@@ -654,7 +658,7 @@ program.command("upload").description("upload a file; prints markdown to referen
|
|
|
654
658
|
});
|
|
655
659
|
});
|
|
656
660
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
657
|
-
const { startMcpServer } = await import("./server-
|
|
661
|
+
const { startMcpServer } = await import("./server-GYP5FUKD.js");
|
|
658
662
|
await startMcpServer();
|
|
659
663
|
});
|
|
660
664
|
program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option("--timeout <ms>", "how long to wait for the browser callback", integer).option("--json", "emit JSON").action(async (opts) => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Client,
|
|
5
5
|
HttpError,
|
|
6
6
|
TransportError,
|
|
7
|
+
UsageError,
|
|
7
8
|
archive,
|
|
8
9
|
blocked,
|
|
9
10
|
close,
|
|
@@ -31,12 +32,13 @@ import {
|
|
|
31
32
|
resolveConfig,
|
|
32
33
|
show,
|
|
33
34
|
stats,
|
|
35
|
+
teamCreate,
|
|
34
36
|
teamList,
|
|
35
37
|
update,
|
|
36
38
|
upload,
|
|
37
39
|
userList,
|
|
38
40
|
workflowStates
|
|
39
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-JOT3TGQQ.js";
|
|
40
42
|
|
|
41
43
|
// src/mcp/server.ts
|
|
42
44
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -105,6 +107,9 @@ var TOOLS = [
|
|
|
105
107
|
scope: z.enum(["all", "archived"]).optional()
|
|
106
108
|
},
|
|
107
109
|
handler: (ctx, args) => {
|
|
110
|
+
if (args["ready"] === true && args["blocked"] === true) {
|
|
111
|
+
throw new UsageError("ready and blocked are mutually exclusive \u2014 pass one, not both");
|
|
112
|
+
}
|
|
108
113
|
if (args["ready"] === true) return ready(ctx, args);
|
|
109
114
|
if (args["blocked"] === true) return blocked(ctx, args);
|
|
110
115
|
return list(ctx, args);
|
|
@@ -379,6 +384,16 @@ var TOOLS = [
|
|
|
379
384
|
shape: {},
|
|
380
385
|
handler: (ctx) => teamList(ctx)
|
|
381
386
|
},
|
|
387
|
+
{
|
|
388
|
+
name: "team_create",
|
|
389
|
+
description: "Create a team. The key becomes the identifier prefix for issues imported under it (upcased, letters/digits, letter first; IO is reserved). The server seeds the default workflow states, so the team is immediately usable.",
|
|
390
|
+
readOnly: false,
|
|
391
|
+
shape: {
|
|
392
|
+
key: z.string().describe("team key, e.g. IOS"),
|
|
393
|
+
name: z.string().describe("display name")
|
|
394
|
+
},
|
|
395
|
+
handler: (ctx, args) => teamCreate(ctx, { key: args["key"], name: args["name"] })
|
|
396
|
+
},
|
|
382
397
|
{
|
|
383
398
|
name: "workflow_states",
|
|
384
399
|
description: "A team's workflow states (name + type).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allocator-one/harness-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI and MCP server for Harness, allocator.one's issue tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup",
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
|
+
"pretest": "npm run build",
|
|
27
28
|
"test": "vitest run",
|
|
28
29
|
"codegen": "openapi-typescript spec/openapi.json -o src/generated/api.ts",
|
|
29
30
|
"spec:refresh": "node scripts/spec-refresh.mjs",
|
package/spec/openapi.json
CHANGED
|
@@ -1866,6 +1866,53 @@
|
|
|
1866
1866
|
},
|
|
1867
1867
|
"summary": "List teams",
|
|
1868
1868
|
"tags": []
|
|
1869
|
+
},
|
|
1870
|
+
"post": {
|
|
1871
|
+
"callbacks": {},
|
|
1872
|
+
"deprecated": false,
|
|
1873
|
+
"description": "Requires the `harness:write` scope. Creates a team seeded with the default workflow states (Backlog, Todo, In Progress, Done, Canceled), so it is immediately usable for the full issue lifecycle. The key is upcased, must match `^[A-Z][A-Z0-9]*$`, and `IO` is reserved for the issues Harness mints.",
|
|
1874
|
+
"parameters": [],
|
|
1875
|
+
"requestBody": {
|
|
1876
|
+
"content": {
|
|
1877
|
+
"application/json": {
|
|
1878
|
+
"schema": {
|
|
1879
|
+
"properties": {
|
|
1880
|
+
"key": {
|
|
1881
|
+
"description": "Identifier prefix for issues imported under the team (e.g. ALL). Upcased; create-only.",
|
|
1882
|
+
"type": "string"
|
|
1883
|
+
},
|
|
1884
|
+
"name": {
|
|
1885
|
+
"type": "string"
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
"type": "object"
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
},
|
|
1892
|
+
"required": true
|
|
1893
|
+
},
|
|
1894
|
+
"responses": {
|
|
1895
|
+
"200": {
|
|
1896
|
+
"description": "Success"
|
|
1897
|
+
},
|
|
1898
|
+
"400": {
|
|
1899
|
+
"description": "Bad request"
|
|
1900
|
+
},
|
|
1901
|
+
"401": {
|
|
1902
|
+
"description": "Missing or invalid token"
|
|
1903
|
+
},
|
|
1904
|
+
"403": {
|
|
1905
|
+
"description": "Token not permitted (org-scoped, non-admin, or missing scope)"
|
|
1906
|
+
},
|
|
1907
|
+
"404": {
|
|
1908
|
+
"description": "Not found"
|
|
1909
|
+
},
|
|
1910
|
+
"422": {
|
|
1911
|
+
"description": "Validation error"
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
"summary": "Create a team",
|
|
1915
|
+
"tags": []
|
|
1869
1916
|
}
|
|
1870
1917
|
},
|
|
1871
1918
|
"/api/v1/teams/{team_key}/issues": {
|