@allocator-one/harness-cli 1.0.1 → 1.2.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 +19 -1
- package/dist/{chunk-BGBBEM3L.js → chunk-JMGBMBN6.js} +141 -12
- package/dist/cli.js +57 -5
- package/dist/{server-YNJDA7IY.js → server-W4SXUKVP.js} +13 -1
- package/package.json +1 -1
- package/spec/openapi.json +47 -0
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:
|
|
@@ -73,6 +73,24 @@ Auth resolution is identical to the CLI. Without a credential, tools return
|
|
|
73
73
|
an error telling the agent to run `harness login` in a terminal — the
|
|
74
74
|
server never opens a browser itself.
|
|
75
75
|
|
|
76
|
+
## Repo config
|
|
77
|
+
|
|
78
|
+
A committed `.harness-cli/config.json`, discovered git-style by walking up
|
|
79
|
+
from the working directory, gives every clone of a repo its defaults:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "team": "CLI" }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Field | Effect |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `team` | Default team key. Precedence: `--team` > `HARNESS_TEAM` > repo config. |
|
|
88
|
+
| `url` | Target host. `HARNESS_API_URL` wins. Tokens stay host-bound: a stored login only serves the host that minted it, an env `HARNESS_API_TOKEN` refuses to pair with a repo-named host (set `HARNESS_API_URL` explicitly), and before the interactive auto-login opens a browser at a host named only by the repo, the CLI asks once per host (remembered in `~/.config/harness/trusted-hosts.json`). |
|
|
89
|
+
| `org` | Reserved for Harness org-scoping; parsed today, `url` wins meanwhile. |
|
|
90
|
+
|
|
91
|
+
This is config, not state — issues stay server-side. The escript does not
|
|
92
|
+
read this file (TS-CLI-only).
|
|
93
|
+
|
|
76
94
|
## Development
|
|
77
95
|
|
|
78
96
|
```sh
|
|
@@ -1250,6 +1250,24 @@ async function labelCreate(ctx, name, color) {
|
|
|
1250
1250
|
return { json: response.data, human: `Created label ${clean(response.data["name"])}
|
|
1251
1251
|
` };
|
|
1252
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
|
+
}
|
|
1253
1271
|
async function teamList(ctx) {
|
|
1254
1272
|
const body = await ctx.client.get("/api/v1/teams");
|
|
1255
1273
|
const human = body.data.length === 0 ? "No teams.\n" : table(
|
|
@@ -1319,18 +1337,119 @@ function deleteCredentials(path2) {
|
|
|
1319
1337
|
rmSync2(path2, { force: true });
|
|
1320
1338
|
}
|
|
1321
1339
|
|
|
1340
|
+
// src/core/repo-config.ts
|
|
1341
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1342
|
+
import { homedir as homedir2 } from "os";
|
|
1343
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
|
|
1344
|
+
var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
|
|
1345
|
+
function findRepoConfig(startDir = process.cwd()) {
|
|
1346
|
+
let dir = startDir;
|
|
1347
|
+
const { root } = parse(startDir);
|
|
1348
|
+
for (; ; ) {
|
|
1349
|
+
const candidate = join3(dir, CONFIG_RELATIVE_PATH);
|
|
1350
|
+
if (existsSync(candidate)) {
|
|
1351
|
+
return readRepoConfig(candidate);
|
|
1352
|
+
}
|
|
1353
|
+
if (dir === root) return { config: null, warnings: [] };
|
|
1354
|
+
dir = dirname3(dir);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function readRepoConfig(path2) {
|
|
1358
|
+
let parsed;
|
|
1359
|
+
try {
|
|
1360
|
+
parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1363
|
+
return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1366
|
+
return { config: null, warnings: [`harness: ignoring ${path2} \u2014 expected a JSON object`] };
|
|
1367
|
+
}
|
|
1368
|
+
const record = parsed;
|
|
1369
|
+
const config = { path: path2 };
|
|
1370
|
+
const warnings = [];
|
|
1371
|
+
for (const field2 of ["team", "url", "org"]) {
|
|
1372
|
+
const value = record[field2];
|
|
1373
|
+
if (value === void 0) continue;
|
|
1374
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1375
|
+
config[field2] = value.trim();
|
|
1376
|
+
} else {
|
|
1377
|
+
warnings.push(`harness: ignoring "${field2}" in ${path2} \u2014 expected a non-empty string`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return { config, warnings };
|
|
1381
|
+
}
|
|
1382
|
+
function trustedHostsPath(env = process.env) {
|
|
1383
|
+
const xdg = env["XDG_CONFIG_HOME"];
|
|
1384
|
+
const base = xdg !== void 0 && xdg !== "" && isAbsolute2(xdg) ? xdg : join3(homedir2(), ".config");
|
|
1385
|
+
return join3(base, "harness", "trusted-hosts.json");
|
|
1386
|
+
}
|
|
1387
|
+
function readTrustedHosts(env) {
|
|
1388
|
+
try {
|
|
1389
|
+
const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
|
|
1390
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
|
|
1391
|
+
return parsed["hosts"].filter(
|
|
1392
|
+
(host) => typeof host === "string"
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
return [];
|
|
1398
|
+
}
|
|
1399
|
+
function isHostTrusted(baseUrl, env) {
|
|
1400
|
+
return readTrustedHosts(env).includes(baseUrl);
|
|
1401
|
+
}
|
|
1402
|
+
function trustHost(baseUrl, env) {
|
|
1403
|
+
const hosts = readTrustedHosts(env);
|
|
1404
|
+
if (hosts.includes(baseUrl)) return;
|
|
1405
|
+
const path2 = trustedHostsPath(env);
|
|
1406
|
+
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1407
|
+
writeFileSync3(path2, JSON.stringify({ hosts: [...hosts, baseUrl] }, null, 2) + "\n");
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1322
1410
|
// src/core/config.ts
|
|
1323
|
-
import { homedir as
|
|
1324
|
-
import { isAbsolute as
|
|
1411
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
1412
|
+
import { isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
1325
1413
|
var DEFAULT_BASE_URL = "https://harness.infra.one";
|
|
1326
|
-
function resolveConfig(env = process.env, credentialsPath) {
|
|
1414
|
+
function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
1415
|
+
let warnings = [];
|
|
1416
|
+
if (repo === void 0) {
|
|
1417
|
+
const lookup = findRepoConfig();
|
|
1418
|
+
repo = lookup.config;
|
|
1419
|
+
warnings = lookup.warnings;
|
|
1420
|
+
}
|
|
1327
1421
|
const path2 = credentialsPath ?? defaultCredentialsPath(env);
|
|
1328
1422
|
const stored = readCredentials(path2);
|
|
1329
1423
|
const envUrl = blankToNull(env["HARNESS_API_URL"]);
|
|
1330
1424
|
const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
|
|
1331
|
-
const
|
|
1425
|
+
const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
|
|
1426
|
+
let rawUrl;
|
|
1427
|
+
let hostSource;
|
|
1428
|
+
if (envUrl !== null) {
|
|
1429
|
+
rawUrl = envUrl;
|
|
1430
|
+
hostSource = "env";
|
|
1431
|
+
} else if (envToken !== null) {
|
|
1432
|
+
if (repoUrl !== null && normalize(repoUrl) !== normalize(DEFAULT_BASE_URL)) {
|
|
1433
|
+
return {
|
|
1434
|
+
kind: "error",
|
|
1435
|
+
message: `this repo's config (${repo?.path}) targets ${repoUrl}, but HARNESS_API_TOKEN only travels to the host the environment names \u2014 set HARNESS_API_URL explicitly to pair them`,
|
|
1436
|
+
warnings
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
rawUrl = DEFAULT_BASE_URL;
|
|
1440
|
+
hostSource = "default";
|
|
1441
|
+
} else if (repoUrl !== null) {
|
|
1442
|
+
rawUrl = repoUrl;
|
|
1443
|
+
hostSource = "repo-config";
|
|
1444
|
+
} else if (stored !== null) {
|
|
1445
|
+
rawUrl = stored.url;
|
|
1446
|
+
hostSource = "stored-login";
|
|
1447
|
+
} else {
|
|
1448
|
+
rawUrl = DEFAULT_BASE_URL;
|
|
1449
|
+
hostSource = "default";
|
|
1450
|
+
}
|
|
1332
1451
|
const validated = validateBaseUrl(rawUrl);
|
|
1333
|
-
if (!validated.ok) return { kind: "error", message: validated.message };
|
|
1452
|
+
if (!validated.ok) return { kind: "error", message: validated.message, warnings };
|
|
1334
1453
|
const baseUrl = rawUrl.replace(/\/+$/, "");
|
|
1335
1454
|
let token;
|
|
1336
1455
|
if (envToken !== null) {
|
|
@@ -1338,7 +1457,9 @@ function resolveConfig(env = process.env, credentialsPath) {
|
|
|
1338
1457
|
} else if (stored !== null && normalize(stored.url) !== null && normalize(stored.url) === normalize(baseUrl)) {
|
|
1339
1458
|
token = stored.token;
|
|
1340
1459
|
} else {
|
|
1341
|
-
|
|
1460
|
+
const result = { kind: "needs-login", baseUrl, hostSource, warnings };
|
|
1461
|
+
if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
|
|
1462
|
+
return result;
|
|
1342
1463
|
}
|
|
1343
1464
|
return {
|
|
1344
1465
|
kind: "ok",
|
|
@@ -1346,19 +1467,23 @@ function resolveConfig(env = process.env, credentialsPath) {
|
|
|
1346
1467
|
baseUrl,
|
|
1347
1468
|
token,
|
|
1348
1469
|
actor: blankToNull(env["HARNESS_ACTOR"]),
|
|
1349
|
-
team:
|
|
1350
|
-
|
|
1351
|
-
|
|
1470
|
+
// The committed team is the fallback: the environment (and, above it,
|
|
1471
|
+
// the --team flag at the command layer) stays authoritative.
|
|
1472
|
+
team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
|
|
1473
|
+
cacheDir: resolveCacheDir(env),
|
|
1474
|
+
hostSource
|
|
1475
|
+
},
|
|
1476
|
+
warnings
|
|
1352
1477
|
};
|
|
1353
1478
|
}
|
|
1354
1479
|
function resolveCacheDir(env = process.env) {
|
|
1355
|
-
return absolute(env["HARNESS_CACHE_DIR"]) ??
|
|
1480
|
+
return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(cacheHome(env), "harness", "attachments");
|
|
1356
1481
|
}
|
|
1357
1482
|
function absolute(value) {
|
|
1358
|
-
return value !== void 0 && value !== "" &&
|
|
1483
|
+
return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
|
|
1359
1484
|
}
|
|
1360
1485
|
function cacheHome(env) {
|
|
1361
|
-
return absolute(env["XDG_CACHE_HOME"]) ??
|
|
1486
|
+
return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
|
|
1362
1487
|
}
|
|
1363
1488
|
function normalize(url) {
|
|
1364
1489
|
let parsed;
|
|
@@ -1456,6 +1581,7 @@ export {
|
|
|
1456
1581
|
linkRm,
|
|
1457
1582
|
labelList,
|
|
1458
1583
|
labelCreate,
|
|
1584
|
+
teamCreate,
|
|
1459
1585
|
teamList,
|
|
1460
1586
|
workflowStates,
|
|
1461
1587
|
userList,
|
|
@@ -1464,6 +1590,9 @@ export {
|
|
|
1464
1590
|
readCredentials,
|
|
1465
1591
|
writeCredentials,
|
|
1466
1592
|
deleteCredentials,
|
|
1593
|
+
findRepoConfig,
|
|
1594
|
+
isHostTrusted,
|
|
1595
|
+
trustHost,
|
|
1467
1596
|
DEFAULT_BASE_URL,
|
|
1468
1597
|
resolveConfig,
|
|
1469
1598
|
validateBaseUrl,
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
docList,
|
|
24
24
|
docShow,
|
|
25
25
|
docUpdate,
|
|
26
|
+
findRepoConfig,
|
|
27
|
+
isHostTrusted,
|
|
26
28
|
json,
|
|
27
29
|
labelCreate,
|
|
28
30
|
labelList,
|
|
@@ -45,7 +47,9 @@ import {
|
|
|
45
47
|
search,
|
|
46
48
|
show,
|
|
47
49
|
stats,
|
|
50
|
+
teamCreate,
|
|
48
51
|
teamList,
|
|
52
|
+
trustHost,
|
|
49
53
|
update,
|
|
50
54
|
upload,
|
|
51
55
|
userList,
|
|
@@ -53,9 +57,10 @@ import {
|
|
|
53
57
|
version,
|
|
54
58
|
workflowStates,
|
|
55
59
|
writeCredentials
|
|
56
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-JMGBMBN6.js";
|
|
57
61
|
|
|
58
62
|
// src/cli.ts
|
|
63
|
+
import { createInterface as createInterface2 } from "readline";
|
|
59
64
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
60
65
|
|
|
61
66
|
// src/core/auth.ts
|
|
@@ -313,10 +318,23 @@ async function whoami(config, credentialSource) {
|
|
|
313
318
|
throw error;
|
|
314
319
|
}
|
|
315
320
|
const serverVersion = await client.serverVersion().catch(() => "unknown");
|
|
321
|
+
const hostSourceLabels = {
|
|
322
|
+
env: "HARNESS_API_URL",
|
|
323
|
+
"repo-config": ".harness-cli/config.json",
|
|
324
|
+
"stored-login": "stored login",
|
|
325
|
+
default: "workspace default"
|
|
326
|
+
};
|
|
327
|
+
const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
|
|
316
328
|
return {
|
|
317
329
|
ok: true,
|
|
318
|
-
json: {
|
|
319
|
-
|
|
330
|
+
json: {
|
|
331
|
+
url: config.baseUrl,
|
|
332
|
+
source: credentialSource,
|
|
333
|
+
verified: true,
|
|
334
|
+
server_version: serverVersion,
|
|
335
|
+
host_source: config.hostSource
|
|
336
|
+
},
|
|
337
|
+
human: `Host: ${config.baseUrl}${via}
|
|
320
338
|
Credential: ${credentialSource}
|
|
321
339
|
Verified: ok (server API ${serverVersion})
|
|
322
340
|
`
|
|
@@ -353,6 +371,15 @@ function defaultOpenBrowser(url) {
|
|
|
353
371
|
}
|
|
354
372
|
|
|
355
373
|
// src/cli.ts
|
|
374
|
+
function promptLine(question) {
|
|
375
|
+
const readline = createInterface2({ input: process.stdin, output: process.stderr });
|
|
376
|
+
return new Promise((resolve) => {
|
|
377
|
+
readline.question(question, (answer) => {
|
|
378
|
+
readline.close();
|
|
379
|
+
resolve(answer);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
356
383
|
function exitWith(code) {
|
|
357
384
|
const drain = (stream) => new Promise((resolve) => stream.write("", () => resolve()));
|
|
358
385
|
return Promise.all([drain(process.stdout), drain(process.stderr)]).then(() => process.exit(code));
|
|
@@ -376,6 +403,7 @@ function interactive() {
|
|
|
376
403
|
}
|
|
377
404
|
async function withConfig(jsonFlag, mutating, run) {
|
|
378
405
|
const resolved = resolveConfig();
|
|
406
|
+
for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
|
|
379
407
|
if (resolved.kind === "error") {
|
|
380
408
|
process.stderr.write(`harness: ${resolved.message}
|
|
381
409
|
`);
|
|
@@ -386,6 +414,19 @@ async function withConfig(jsonFlag, mutating, run) {
|
|
|
386
414
|
process.stderr.write(loginHelp(resolved.baseUrl));
|
|
387
415
|
return exitWith(2);
|
|
388
416
|
}
|
|
417
|
+
if (resolved.hostSource === "repo-config" && !isHostTrusted(resolved.baseUrl)) {
|
|
418
|
+
const answer = await promptLine(
|
|
419
|
+
`This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
|
|
420
|
+
);
|
|
421
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
422
|
+
process.stderr.write(
|
|
423
|
+
`harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
424
|
+
`
|
|
425
|
+
);
|
|
426
|
+
return exitWith(2);
|
|
427
|
+
}
|
|
428
|
+
trustHost(resolved.baseUrl);
|
|
429
|
+
}
|
|
389
430
|
const loginResult = await login(resolved.baseUrl, {});
|
|
390
431
|
if (!loginResult.ok) {
|
|
391
432
|
process.stderr.write(`harness: ${clean(loginResult.message)}
|
|
@@ -640,6 +681,9 @@ var team = program.command("team").description("teams");
|
|
|
640
681
|
team.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
641
682
|
await withConfig(opts.json === true, false, (ctx) => teamList(ctx));
|
|
642
683
|
});
|
|
684
|
+
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) => {
|
|
685
|
+
await withConfig(opts.json === true, true, (ctx) => teamCreate(ctx, opts));
|
|
686
|
+
});
|
|
643
687
|
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
688
|
await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
|
|
645
689
|
});
|
|
@@ -654,11 +698,18 @@ program.command("upload").description("upload a file; prints markdown to referen
|
|
|
654
698
|
});
|
|
655
699
|
});
|
|
656
700
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
657
|
-
const { startMcpServer } = await import("./server-
|
|
701
|
+
const { startMcpServer } = await import("./server-W4SXUKVP.js");
|
|
658
702
|
await startMcpServer();
|
|
659
703
|
});
|
|
660
704
|
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) => {
|
|
661
|
-
const
|
|
705
|
+
const repoLookup = findRepoConfig();
|
|
706
|
+
for (const warning of repoLookup.warnings) process.stderr.write(warning + "\n");
|
|
707
|
+
const repoUrl = repoLookup.config?.url;
|
|
708
|
+
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
709
|
+
if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
|
|
710
|
+
process.stderr.write(`harness: host ${url} comes from ${repoLookup.config?.path}
|
|
711
|
+
`);
|
|
712
|
+
}
|
|
662
713
|
const options = { noBrowser: !opts.browser };
|
|
663
714
|
if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
|
|
664
715
|
await finishAuth(await login(url, options), opts.json === true);
|
|
@@ -668,6 +719,7 @@ program.command("logout").description("revoke and forget the stored login").opti
|
|
|
668
719
|
});
|
|
669
720
|
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
670
721
|
const resolved = resolveConfig();
|
|
722
|
+
for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
|
|
671
723
|
if (resolved.kind === "error") {
|
|
672
724
|
process.stderr.write(`harness: ${resolved.message}
|
|
673
725
|
`);
|
|
@@ -32,12 +32,13 @@ import {
|
|
|
32
32
|
resolveConfig,
|
|
33
33
|
show,
|
|
34
34
|
stats,
|
|
35
|
+
teamCreate,
|
|
35
36
|
teamList,
|
|
36
37
|
update,
|
|
37
38
|
upload,
|
|
38
39
|
userList,
|
|
39
40
|
workflowStates
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-JMGBMBN6.js";
|
|
41
42
|
|
|
42
43
|
// src/mcp/server.ts
|
|
43
44
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -47,6 +48,7 @@ var NeedsLoginError = class extends Error {
|
|
|
47
48
|
};
|
|
48
49
|
function context() {
|
|
49
50
|
const resolved = resolveConfig();
|
|
51
|
+
for (const warning of resolved.warnings) process.stderr.write(warning + "\n");
|
|
50
52
|
if (resolved.kind === "error") throw new NeedsLoginError(resolved.message);
|
|
51
53
|
if (resolved.kind === "needs-login") {
|
|
52
54
|
throw new NeedsLoginError(
|
|
@@ -383,6 +385,16 @@ var TOOLS = [
|
|
|
383
385
|
shape: {},
|
|
384
386
|
handler: (ctx) => teamList(ctx)
|
|
385
387
|
},
|
|
388
|
+
{
|
|
389
|
+
name: "team_create",
|
|
390
|
+
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.",
|
|
391
|
+
readOnly: false,
|
|
392
|
+
shape: {
|
|
393
|
+
key: z.string().describe("team key, e.g. IOS"),
|
|
394
|
+
name: z.string().describe("display name")
|
|
395
|
+
},
|
|
396
|
+
handler: (ctx, args) => teamCreate(ctx, { key: args["key"], name: args["name"] })
|
|
397
|
+
},
|
|
386
398
|
{
|
|
387
399
|
name: "workflow_states",
|
|
388
400
|
description: "A team's workflow states (name + type).",
|
package/package.json
CHANGED
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": {
|