tines 0.0.140 → 0.0.142
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/dist/index.js +503 -63
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3716,6 +3716,44 @@ function displayActor(ev) {
|
|
|
3716
3716
|
return actorLabel(ev.actor);
|
|
3717
3717
|
}
|
|
3718
3718
|
|
|
3719
|
+
// ../shared/src/requirements.ts
|
|
3720
|
+
function attachFlag(type, slot, contentType) {
|
|
3721
|
+
switch (type) {
|
|
3722
|
+
case "file":
|
|
3723
|
+
return "--file <path>";
|
|
3724
|
+
case "folder":
|
|
3725
|
+
return "--folder <dir>";
|
|
3726
|
+
case "link":
|
|
3727
|
+
return "--link <url>";
|
|
3728
|
+
case "pr":
|
|
3729
|
+
return "--pr <owner/repo#N>";
|
|
3730
|
+
case "text": {
|
|
3731
|
+
const ext = contentType === "text/markdown" ? "md" : contentType === "text/plain" ? "txt" : null;
|
|
3732
|
+
return ext ? `--text @${slot}.${ext}` : "--text <markdown|@file>";
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
function requirementFix(r, ref) {
|
|
3737
|
+
const attach = (type) => `tines issues artifacts attach ${ref} ${r.artifact} ${attachFlag(type, r.artifact, r.content_type)}`;
|
|
3738
|
+
if (r.status === "missing" || r.current_type === null) {
|
|
3739
|
+
return { kind: "attach", command: attach(r.type ?? "file") };
|
|
3740
|
+
}
|
|
3741
|
+
if (r.status === "satisfied") {
|
|
3742
|
+
return { kind: "attach", command: attach(r.current_type) };
|
|
3743
|
+
}
|
|
3744
|
+
if (r.status === "stale") {
|
|
3745
|
+
return {
|
|
3746
|
+
kind: "reattach_or_reaffirm",
|
|
3747
|
+
command: `${attach(r.current_type)} \u2014 or, if the current content still stands: tines issues artifacts reaffirm ${ref} ${r.artifact}`
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
const reattachable = r.type === void 0 ? r.current_type === "file" || r.current_type === "text" : r.current_type === r.type;
|
|
3751
|
+
return reattachable ? { kind: "attach", command: attach(r.current_type) } : {
|
|
3752
|
+
kind: "delete_and_attach",
|
|
3753
|
+
command: `tines issues artifacts delete ${ref} ${r.artifact} && ${attach(r.type ?? "file")}`
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3719
3757
|
// ../shared/src/client.ts
|
|
3720
3758
|
var ApiError = class extends Error {
|
|
3721
3759
|
status;
|
|
@@ -4149,6 +4187,29 @@ function artifactSummary(a) {
|
|
|
4149
4187
|
return `${prRefLabel(cv)} \u2014 ${cv.pr_repo_url}/pull/${cv.pr_number}`;
|
|
4150
4188
|
}
|
|
4151
4189
|
}
|
|
4190
|
+
function artifactTypeLabel(a) {
|
|
4191
|
+
const ct = a.current_version.content_type;
|
|
4192
|
+
return (a.artifact_type === "file" || a.artifact_type === "text") && ct ? `${a.artifact_type}, ${ct}` : a.artifact_type;
|
|
4193
|
+
}
|
|
4194
|
+
function requirementStatus(r) {
|
|
4195
|
+
switch (r.status) {
|
|
4196
|
+
case "satisfied":
|
|
4197
|
+
return `satisfied (v${r.current_version?.version})`;
|
|
4198
|
+
case "missing":
|
|
4199
|
+
return "missing";
|
|
4200
|
+
case "stale":
|
|
4201
|
+
return `stale (v${r.current_version?.version}, attached before the current state)`;
|
|
4202
|
+
case "type_mismatch":
|
|
4203
|
+
return r.type !== void 0 && r.current_type !== null && r.current_type !== r.type ? `type mismatch (holds ${r.current_type})` : `type mismatch (v${r.current_version?.version} is not ${r.content_type ?? r.type})`;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
function requirementLines(r) {
|
|
4207
|
+
const spec = [r.type, r.content_type].filter(Boolean).join(", ");
|
|
4208
|
+
return [
|
|
4209
|
+
`requires artifact "${r.artifact}"${spec ? ` (${spec})` : ""}: ${requirementStatus(r)}${r.description ? ` \u2014 ${r.description}` : ""}`,
|
|
4210
|
+
...r.fix ? [` fix: ${r.fix}`] : []
|
|
4211
|
+
];
|
|
4212
|
+
}
|
|
4152
4213
|
function scheduleRef(s) {
|
|
4153
4214
|
return `${s.project_name}/${s.name}`;
|
|
4154
4215
|
}
|
|
@@ -4406,12 +4467,10 @@ allowed actions: ${actions.join(", ")}` : "\nallowed actions: none (terminal sta
|
|
|
4406
4467
|
const unmet = err.details?.unmet;
|
|
4407
4468
|
if (Array.isArray(unmet)) {
|
|
4408
4469
|
for (const raw of unmet) {
|
|
4409
|
-
const
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
if (r.fix) message3 += `
|
|
4414
|
-
fix: ${r.fix}`;
|
|
4470
|
+
for (const line of requirementLines(raw)) {
|
|
4471
|
+
message3 += `
|
|
4472
|
+
${line}`;
|
|
4473
|
+
}
|
|
4415
4474
|
}
|
|
4416
4475
|
}
|
|
4417
4476
|
die(message3);
|
|
@@ -4748,9 +4807,334 @@ function register(program3) {
|
|
|
4748
4807
|
}
|
|
4749
4808
|
|
|
4750
4809
|
// src/commands/issues.ts
|
|
4751
|
-
import { existsSync as
|
|
4810
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4752
4811
|
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
4753
4812
|
|
|
4813
|
+
// src/attach-source.ts
|
|
4814
|
+
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
4815
|
+
var fsProbe = (path2) => {
|
|
4816
|
+
if (!existsSync2(path2)) return "missing";
|
|
4817
|
+
try {
|
|
4818
|
+
return statSync(path2).isDirectory() ? "dir" : "file";
|
|
4819
|
+
} catch {
|
|
4820
|
+
return "missing";
|
|
4821
|
+
}
|
|
4822
|
+
};
|
|
4823
|
+
var SOURCE_FLAGS = ["--file", "--folder", "--text", "--link", "--pr"];
|
|
4824
|
+
function gatesFor(issue, name2) {
|
|
4825
|
+
const gates = [];
|
|
4826
|
+
for (const t of issue.allowed_transitions) {
|
|
4827
|
+
for (const check of t.requires ?? []) {
|
|
4828
|
+
if (name2 === void 0 || check.artifact === name2) {
|
|
4829
|
+
gates.push({ transition: t.name, check });
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
return gates;
|
|
4834
|
+
}
|
|
4835
|
+
function flagFor(type) {
|
|
4836
|
+
return type === "file" ? "--file" : `--${type}`;
|
|
4837
|
+
}
|
|
4838
|
+
function sourceLabel(flags, positional) {
|
|
4839
|
+
if (flags.file !== void 0) return "--file";
|
|
4840
|
+
if (flags.folder !== void 0) return "--folder";
|
|
4841
|
+
if (flags.text !== void 0) return "--text";
|
|
4842
|
+
if (flags.link !== void 0) return "--link";
|
|
4843
|
+
if (flags.pr !== void 0) return "--pr";
|
|
4844
|
+
return positional !== void 0 ? "the positional source" : "the source";
|
|
4845
|
+
}
|
|
4846
|
+
function assertOneSource(flags, positional) {
|
|
4847
|
+
const values = [flags.file, flags.folder, flags.text, flags.link, flags.pr];
|
|
4848
|
+
const given = SOURCE_FLAGS.filter((_, i) => values[i] !== void 0);
|
|
4849
|
+
const count = given.length + (positional !== void 0 ? 1 : 0);
|
|
4850
|
+
if (count === 0) {
|
|
4851
|
+
throw new CliError(
|
|
4852
|
+
"pass exactly one content source: a positional <source> (a path, a URL, or owner/repo#N), --file <path>, --folder <dir>, --text <md|@file>, --link <url>, or --pr <spec> (a link goes in --link; --url is the API base URL)"
|
|
4853
|
+
);
|
|
4854
|
+
}
|
|
4855
|
+
if (count > 1) {
|
|
4856
|
+
const what = [
|
|
4857
|
+
...given,
|
|
4858
|
+
...positional !== void 0 ? [`the positional "${positional}"`] : []
|
|
4859
|
+
];
|
|
4860
|
+
throw new CliError(`pass the source once: got ${what.join(" and ")}`);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
function normalizePositional(raw) {
|
|
4864
|
+
if (raw === "-" || raw === "@-") return { value: raw, stdin: true };
|
|
4865
|
+
if (raw.startsWith("@@")) return { value: raw.slice(1), stdin: false };
|
|
4866
|
+
if (raw.startsWith("@")) return { value: raw.slice(1), stdin: false };
|
|
4867
|
+
return { value: raw, stdin: false };
|
|
4868
|
+
}
|
|
4869
|
+
var isUrl = (v) => /^https?:\/\//.test(v);
|
|
4870
|
+
function concreteContentType(ct) {
|
|
4871
|
+
return ct !== void 0 && ct.includes("/") && !ct.endsWith("/") ? ct : void 0;
|
|
4872
|
+
}
|
|
4873
|
+
function declaredTypes(gates) {
|
|
4874
|
+
return [
|
|
4875
|
+
...new Set(gates.map((g) => g.check.type).filter((t) => t !== void 0))
|
|
4876
|
+
];
|
|
4877
|
+
}
|
|
4878
|
+
function shapeCompatible(types, value, stdin, probe) {
|
|
4879
|
+
if (stdin) return types.filter((t) => t === "text" || t === "file");
|
|
4880
|
+
const what = probe(value);
|
|
4881
|
+
if (what === "dir") return types.filter((t) => t === "folder");
|
|
4882
|
+
if (what === "file") return types.filter((t) => t === "text" || t === "file");
|
|
4883
|
+
if (isUrl(value)) {
|
|
4884
|
+
return types.filter((t) => t === "link" || t === "pr" && parsePrSpec(value) !== null);
|
|
4885
|
+
}
|
|
4886
|
+
return types.filter((t) => t === "pr" && parsePrSpec(value) !== null);
|
|
4887
|
+
}
|
|
4888
|
+
function gateSpec(check) {
|
|
4889
|
+
const type = check.type ?? "any type";
|
|
4890
|
+
return check.content_type ? `${type} (${check.content_type})` : type;
|
|
4891
|
+
}
|
|
4892
|
+
function joinTransitions(names) {
|
|
4893
|
+
const unique = [...new Set(names)].map((n) => `"${n}"`);
|
|
4894
|
+
if (unique.length <= 1) return unique.join("");
|
|
4895
|
+
return `${unique.slice(0, -1).join(", ")} and ${unique[unique.length - 1]}`;
|
|
4896
|
+
}
|
|
4897
|
+
function fixCommand(gate, ref) {
|
|
4898
|
+
return gate.check.fix || requirementFix(gate.check, ref).command;
|
|
4899
|
+
}
|
|
4900
|
+
function accepts(gate, type, contentType) {
|
|
4901
|
+
const check = gate.check;
|
|
4902
|
+
if (check.type !== void 0 && check.type !== type) return false;
|
|
4903
|
+
if (check.content_type === void 0) return true;
|
|
4904
|
+
if (contentType === void 0) return true;
|
|
4905
|
+
return contentType.startsWith(check.content_type);
|
|
4906
|
+
}
|
|
4907
|
+
function effectiveContentType(type, source, explicit, sniff) {
|
|
4908
|
+
if (explicit !== void 0) return explicit;
|
|
4909
|
+
if (type === "text") return "text/markdown";
|
|
4910
|
+
if (source.kind === "file-path") return sniff(source.path);
|
|
4911
|
+
if (source.kind === "file-stdin") return "application/octet-stream";
|
|
4912
|
+
return void 0;
|
|
4913
|
+
}
|
|
4914
|
+
function planAttach(input, sniff) {
|
|
4915
|
+
const { ref, name: name2, positional, flags, probe } = input;
|
|
4916
|
+
assertOneSource(flags, positional);
|
|
4917
|
+
const gates = flags.ignoreGates ? [] : input.gates;
|
|
4918
|
+
const plan = positional !== void 0 ? planPositional(name2, positional, gates, probe, flags) : planFlags(flags, probe);
|
|
4919
|
+
if (gates.length > 0) {
|
|
4920
|
+
checkAccepted(plan, gates, ref, name2, flags, positional, sniff);
|
|
4921
|
+
checkExistingSlot(plan, gates, ref, name2, flags, positional);
|
|
4922
|
+
}
|
|
4923
|
+
return plan;
|
|
4924
|
+
}
|
|
4925
|
+
function planPositional(name2, raw, gates, probe, flags) {
|
|
4926
|
+
const { value, stdin } = normalizePositional(raw);
|
|
4927
|
+
const types = declaredTypes(gates);
|
|
4928
|
+
let type;
|
|
4929
|
+
if (types.length === 1) {
|
|
4930
|
+
type = types[0];
|
|
4931
|
+
} else if (types.length > 1) {
|
|
4932
|
+
const compatible = shapeCompatible(types, value, stdin, probe);
|
|
4933
|
+
if (compatible.length === 1) {
|
|
4934
|
+
type = compatible[0];
|
|
4935
|
+
} else {
|
|
4936
|
+
const specs = gates.filter((g) => g.check.type !== void 0).map((g) => `as ${g.check.type} by "${g.transition}"`);
|
|
4937
|
+
throw new CliError(
|
|
4938
|
+
`"${name2}" is gated ${specs.join(" and ")}; pass ${types.map(flagFor).join("/")} to choose`
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
const contentType = flags.contentType ?? gateContentType(gates, type);
|
|
4943
|
+
const withCt = (p) => ({
|
|
4944
|
+
...p,
|
|
4945
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
4946
|
+
...flags.filename !== void 0 ? { filename: flags.filename } : {}
|
|
4947
|
+
});
|
|
4948
|
+
if (type === void 0) {
|
|
4949
|
+
if (stdin) {
|
|
4950
|
+
throw new CliError(
|
|
4951
|
+
`"${name2}" is not gated on this issue, so "-" has no type to read stdin as \u2014 use --text - for a document, or --file <path>`
|
|
4952
|
+
);
|
|
4953
|
+
}
|
|
4954
|
+
const what = probe(value);
|
|
4955
|
+
if (what === "dir") return withCt({ type: "folder", source: { kind: "folder", dir: value } });
|
|
4956
|
+
if (what === "file")
|
|
4957
|
+
return withCt({ type: "file", source: { kind: "file-path", path: value } });
|
|
4958
|
+
const pr = parsePrSpec(value);
|
|
4959
|
+
if (pr) return withCt({ type: "pr", source: { kind: "pr", pr } });
|
|
4960
|
+
if (isUrl(value)) {
|
|
4961
|
+
return withCt({
|
|
4962
|
+
type: "link",
|
|
4963
|
+
source: {
|
|
4964
|
+
kind: "link",
|
|
4965
|
+
url: value,
|
|
4966
|
+
...flags.title !== void 0 ? { title: flags.title } : {}
|
|
4967
|
+
}
|
|
4968
|
+
});
|
|
4969
|
+
}
|
|
4970
|
+
throw new CliError(
|
|
4971
|
+
`no such file "${value}" \u2014 a positional source is a path, a URL or owner/repo#N; inline text goes in --text (text is only inferred under a text gate)`
|
|
4972
|
+
);
|
|
4973
|
+
}
|
|
4974
|
+
switch (type) {
|
|
4975
|
+
case "text": {
|
|
4976
|
+
if (stdin) return withCt({ type: "text", source: { kind: "text-stdin" } });
|
|
4977
|
+
const what = probe(value);
|
|
4978
|
+
if (what === "dir") {
|
|
4979
|
+
throw new CliError(`"${value}" is a directory; "${name2}" is gated as text`);
|
|
4980
|
+
}
|
|
4981
|
+
if (what === "missing") throw new CliError(`cannot read ${value}: no such file`);
|
|
4982
|
+
return withCt({ type: "text", source: { kind: "text-path", path: value } });
|
|
4983
|
+
}
|
|
4984
|
+
case "file": {
|
|
4985
|
+
if (stdin) return withCt({ type: "file", source: { kind: "file-stdin" } });
|
|
4986
|
+
const what = probe(value);
|
|
4987
|
+
if (what === "dir") {
|
|
4988
|
+
throw new CliError(`"${value}" is a directory; "${name2}" is gated as file`);
|
|
4989
|
+
}
|
|
4990
|
+
if (what === "missing") throw new CliError(`cannot read ${value}: no such file`);
|
|
4991
|
+
return withCt({ type: "file", source: { kind: "file-path", path: value } });
|
|
4992
|
+
}
|
|
4993
|
+
case "folder": {
|
|
4994
|
+
if (probe(value) !== "dir") {
|
|
4995
|
+
throw new CliError(`"${name2}" is gated as folder, but "${value}" is not a directory`);
|
|
4996
|
+
}
|
|
4997
|
+
return withCt({ type: "folder", source: { kind: "folder", dir: value } });
|
|
4998
|
+
}
|
|
4999
|
+
case "link": {
|
|
5000
|
+
if (!isUrl(value)) {
|
|
5001
|
+
throw new CliError(`"${name2}" is gated as link, but "${value}" is not an http(s) URL`);
|
|
5002
|
+
}
|
|
5003
|
+
return withCt({
|
|
5004
|
+
type: "link",
|
|
5005
|
+
source: {
|
|
5006
|
+
kind: "link",
|
|
5007
|
+
url: value,
|
|
5008
|
+
...flags.title !== void 0 ? { title: flags.title } : {}
|
|
5009
|
+
}
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
case "pr": {
|
|
5013
|
+
const pr = parsePrSpec(value);
|
|
5014
|
+
if (!pr) {
|
|
5015
|
+
throw new CliError(
|
|
5016
|
+
`"${name2}" is gated as pr, but "${value}" is not owner/repo#N or a GitHub PR URL`
|
|
5017
|
+
);
|
|
5018
|
+
}
|
|
5019
|
+
return withCt({ type: "pr", source: { kind: "pr", pr } });
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
function gateContentType(gates, type) {
|
|
5024
|
+
if (type !== "text" && type !== "file") return void 0;
|
|
5025
|
+
const declared = [
|
|
5026
|
+
...new Set(
|
|
5027
|
+
gates.filter((g) => g.check.type === type).map((g) => concreteContentType(g.check.content_type)).filter((ct) => ct !== void 0)
|
|
5028
|
+
)
|
|
5029
|
+
];
|
|
5030
|
+
return declared.length === 1 ? declared[0] : void 0;
|
|
5031
|
+
}
|
|
5032
|
+
function planFlags(flags, probe) {
|
|
5033
|
+
const ct = flags.contentType;
|
|
5034
|
+
const filename = flags.filename;
|
|
5035
|
+
const modifiers = {
|
|
5036
|
+
...ct !== void 0 ? { contentType: ct } : {},
|
|
5037
|
+
...filename !== void 0 ? { filename } : {}
|
|
5038
|
+
};
|
|
5039
|
+
if (flags.folder !== void 0) {
|
|
5040
|
+
if (probe(flags.folder) !== "dir") {
|
|
5041
|
+
throw new CliError(`--folder needs a directory, got "${flags.folder}"`);
|
|
5042
|
+
}
|
|
5043
|
+
return { type: "folder", source: { kind: "folder", dir: flags.folder }, ...modifiers };
|
|
5044
|
+
}
|
|
5045
|
+
if (flags.file !== void 0) {
|
|
5046
|
+
const what = probe(flags.file);
|
|
5047
|
+
if (what === "missing") throw new CliError(`cannot read ${flags.file}: no such file`);
|
|
5048
|
+
if (what === "dir")
|
|
5049
|
+
throw new CliError(`--file needs a file, got the directory "${flags.file}"`);
|
|
5050
|
+
return { type: "file", source: { kind: "file-path", path: flags.file }, ...modifiers };
|
|
5051
|
+
}
|
|
5052
|
+
if (flags.text !== void 0) {
|
|
5053
|
+
const v = flags.text;
|
|
5054
|
+
const source = v === "-" || v === "@-" ? { kind: "text-stdin" } : v.startsWith("@@") ? { kind: "text-inline", value: v.slice(1) } : v.startsWith("@") ? { kind: "text-path", path: v.slice(1) } : { kind: "text-inline", value: v };
|
|
5055
|
+
return { type: "text", source, ...modifiers };
|
|
5056
|
+
}
|
|
5057
|
+
if (flags.link !== void 0) {
|
|
5058
|
+
return {
|
|
5059
|
+
type: "link",
|
|
5060
|
+
source: {
|
|
5061
|
+
kind: "link",
|
|
5062
|
+
url: flags.link,
|
|
5063
|
+
...flags.title !== void 0 ? { title: flags.title } : {}
|
|
5064
|
+
},
|
|
5065
|
+
...modifiers
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
const parsed = parsePrSpec(flags.pr);
|
|
5069
|
+
if (!parsed) throw new CliError(`--pr takes owner/repo#N or a GitHub PR URL, got "${flags.pr}"`);
|
|
5070
|
+
return { type: "pr", source: { kind: "pr", pr: parsed }, ...modifiers };
|
|
5071
|
+
}
|
|
5072
|
+
function checkExistingSlot(plan, gates, ref, name2, flags, positional) {
|
|
5073
|
+
const held = gates.find((g) => g.check.current_type !== null)?.check.current_type;
|
|
5074
|
+
if (held === void 0 || held === null || held === plan.type) return;
|
|
5075
|
+
const rejecting = gates.find((g) => g.check.type !== void 0 && g.check.type !== held);
|
|
5076
|
+
const fix = rejecting ? fixCommand(rejecting, ref) : `tines issues artifacts delete ${ref} ${name2} && tines issues artifacts attach ${ref} ${name2} ${flagFor(plan.type)} <source>`;
|
|
5077
|
+
throw new CliError(
|
|
5078
|
+
`"${name2}" already holds a ${held} artifact and the type is immutable; ${sourceLabel(flags, positional)} would attach ${plan.type}. Use: ${fix} \u2014 or attach it under a different name (--ignore-gates does not bypass this; the server rejects the type change too)`
|
|
5079
|
+
);
|
|
5080
|
+
}
|
|
5081
|
+
function checkAccepted(plan, gates, ref, name2, flags, positional, sniff) {
|
|
5082
|
+
const effective = effectiveContentType(plan.type, plan.source, plan.contentType, sniff);
|
|
5083
|
+
if (gates.some((g2) => accepts(g2, plan.type, effective))) return;
|
|
5084
|
+
const label = sourceLabel(flags, positional);
|
|
5085
|
+
const typeMatched = gates.filter((g2) => (g2.check.type ?? plan.type) === plan.type);
|
|
5086
|
+
if (typeMatched.length > 0) {
|
|
5087
|
+
const g2 = typeMatched[0];
|
|
5088
|
+
throw new CliError(
|
|
5089
|
+
`"${name2}" is gated by ${joinTransitions(typeMatched.map((x) => x.transition))} as ${gateSpec(g2.check)}; ${label} would attach ${effective ?? plan.type}, which does not satisfy it. Use: ${fixCommand(g2, ref)} with --content-type <mime under ${g2.check.content_type}> (or --ignore-gates to attach it anyway)`
|
|
5090
|
+
);
|
|
5091
|
+
}
|
|
5092
|
+
const g = gates[0];
|
|
5093
|
+
throw new CliError(
|
|
5094
|
+
`"${name2}" is gated by ${joinTransitions(gates.map((x) => x.transition))} as ${gateSpec(g.check)}; ${label} would create a ${plan.type} artifact that can never satisfy it. Use: ${fixCommand(g, ref)} (or --ignore-gates to attach a ${plan.type} anyway)`
|
|
5095
|
+
);
|
|
5096
|
+
}
|
|
5097
|
+
function satisfiedBy(artifact, gates) {
|
|
5098
|
+
const satisfies = [];
|
|
5099
|
+
const rejects = [];
|
|
5100
|
+
for (const gate of gates) {
|
|
5101
|
+
if (gate.check.artifact !== artifact.name) continue;
|
|
5102
|
+
if (accepts(gate, artifact.artifact_type, artifact.current_version.content_type ?? void 0)) {
|
|
5103
|
+
satisfies.push(gate.transition);
|
|
5104
|
+
} else {
|
|
5105
|
+
rejects.push({ transition: gate.transition, wants: wantsLabel(gate, artifact) });
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
return { satisfies, rejects };
|
|
5109
|
+
}
|
|
5110
|
+
function wantsLabel(gate, artifact) {
|
|
5111
|
+
const check = gate.check;
|
|
5112
|
+
if (check.type !== void 0 && check.type !== artifact.artifact_type)
|
|
5113
|
+
return `wants ${check.type}`;
|
|
5114
|
+
return `wants ${check.content_type ?? check.type ?? "something else"}`;
|
|
5115
|
+
}
|
|
5116
|
+
function gateLabel(artifact, gates) {
|
|
5117
|
+
const { rejects } = satisfiedBy(artifact, gates);
|
|
5118
|
+
return [...new Set(rejects.map((r) => r.wants))].join("; ");
|
|
5119
|
+
}
|
|
5120
|
+
var ATTACH_SOURCE_HELP = `
|
|
5121
|
+
Source:
|
|
5122
|
+
A positional <source> is typed by the slot's gate when this issue has one:
|
|
5123
|
+
a text gate reads the path as the document (the gate's content type wins over
|
|
5124
|
+
the extension), a file gate uploads its bytes, a folder gate walks it, and a
|
|
5125
|
+
link/pr gate takes a URL or owner/repo#N. "-" reads stdin under a text or
|
|
5126
|
+
file gate.
|
|
5127
|
+
|
|
5128
|
+
With no gate, the shape alone decides: a directory is a folder, an http(s)
|
|
5129
|
+
URL is a link (a GitHub PR URL is a pr), owner/repo#N is a pr, and anything
|
|
5130
|
+
else is a file \u2014 a .md path included. Inline text is never inferred: pass it
|
|
5131
|
+
with --text.
|
|
5132
|
+
|
|
5133
|
+
Flags still override, and are refused before any network write when no
|
|
5134
|
+
available transition's requirement for the slot could ever accept what they
|
|
5135
|
+
would create. --ignore-gates attaches it anyway. Prefix a source starting
|
|
5136
|
+
with "-" with "--".`;
|
|
5137
|
+
|
|
4754
5138
|
// src/help-guard.ts
|
|
4755
5139
|
function helpGuard(command, markdown) {
|
|
4756
5140
|
if (markdown === "--help" || markdown === "-h") {
|
|
@@ -4879,6 +5263,13 @@ ${issue.description}`);
|
|
|
4879
5263
|
`
|
|
4880
5264
|
allowed actions: ${allowed.length ? allowed.join(", ") : "none (terminal state)"}`
|
|
4881
5265
|
);
|
|
5266
|
+
for (const t of issue.allowed_transitions) {
|
|
5267
|
+
for (const check of t.requires ?? []) {
|
|
5268
|
+
const [head, ...rest] = requirementLines(check);
|
|
5269
|
+
console.log(` "${t.name}" ${head}`);
|
|
5270
|
+
for (const line of rest) console.log(` ${line}`);
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
4882
5273
|
if (issue.comments.length > 0) {
|
|
4883
5274
|
console.log(`
|
|
4884
5275
|
comments (${issue.comments.length}):`);
|
|
@@ -4887,6 +5278,18 @@ comments (${issue.comments.length}):`);
|
|
|
4887
5278
|
}
|
|
4888
5279
|
}
|
|
4889
5280
|
}
|
|
5281
|
+
function readTextSource(source) {
|
|
5282
|
+
if (source.kind === "text-inline") return source.value;
|
|
5283
|
+
if (source.kind === "text-stdin") return readBodyValue("-");
|
|
5284
|
+
if (source.kind === "text-path") {
|
|
5285
|
+
try {
|
|
5286
|
+
return readFileSync4(source.path, "utf8");
|
|
5287
|
+
} catch (err) {
|
|
5288
|
+
die(`cannot read ${source.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
throw new Error(`not a text source: ${source.kind}`);
|
|
5292
|
+
}
|
|
4890
5293
|
function walkFolder(dir) {
|
|
4891
5294
|
const files = [];
|
|
4892
5295
|
const walk = (abs, rel) => {
|
|
@@ -4911,7 +5314,16 @@ function printExplainer(issue, ex) {
|
|
|
4911
5314
|
console.log(`
|
|
4912
5315
|
${ex.verdict}
|
|
4913
5316
|
`);
|
|
4914
|
-
table(
|
|
5317
|
+
table(
|
|
5318
|
+
ex.checks.map((c) => [
|
|
5319
|
+
` ${c.ok ? "ok" : "FAIL"}`,
|
|
5320
|
+
c.name.replaceAll("_", " "),
|
|
5321
|
+
c.detail,
|
|
5322
|
+
// The remedy, when the check has one and it is something to run
|
|
5323
|
+
// here: a link is useless without an origin.
|
|
5324
|
+
c.action?.cli ? `fix: ${c.action.cli}` : ""
|
|
5325
|
+
])
|
|
5326
|
+
);
|
|
4915
5327
|
if (ex.pin) {
|
|
4916
5328
|
console.log(
|
|
4917
5329
|
`
|
|
@@ -5259,7 +5671,7 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
5259
5671
|
`refusing to write: checkout-directory conflict${context.conflicts.length === 1 ? "" : "s"} among the effective repos (${context.conflicts.map((c) => `"${c.dir}": ${c.item_ids.join(", ")}`).join("; ")}); rename or re-dir the items first`
|
|
5260
5672
|
);
|
|
5261
5673
|
}
|
|
5262
|
-
if (
|
|
5674
|
+
if (existsSync3(opts.out) && readdirSync(opts.out).length > 0 && !opts.force) {
|
|
5263
5675
|
die(`refusing to write into non-empty directory ${opts.out} (pass --force to override)`);
|
|
5264
5676
|
}
|
|
5265
5677
|
mkdirSync2(opts.out, { recursive: true });
|
|
@@ -5314,13 +5726,17 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
5314
5726
|
const res = await api.listArtifacts(issue.id);
|
|
5315
5727
|
if (opts.json) return printJson(res);
|
|
5316
5728
|
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
5729
|
+
const gates = gatesFor(issue);
|
|
5730
|
+
const labels = res.items.map((a) => gateLabel(a, gates));
|
|
5731
|
+
const gated = labels.some((l) => l !== "");
|
|
5317
5732
|
table([
|
|
5318
|
-
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
5319
|
-
...res.items.map((a) => [
|
|
5733
|
+
["NAME", "TYPE", "VERSION", "FRESH", ...gated ? ["GATE"] : [], "SUMMARY", "ATTACHED"],
|
|
5734
|
+
...res.items.map((a, i) => [
|
|
5320
5735
|
a.name,
|
|
5321
5736
|
a.artifact_type,
|
|
5322
5737
|
`v${a.current_version.version}`,
|
|
5323
5738
|
a.fresh ? "yes" : "no",
|
|
5739
|
+
...gated ? [labels[i] || "ok"] : [],
|
|
5324
5740
|
artifactSummary(a),
|
|
5325
5741
|
timestamp(a.current_version.created_at)
|
|
5326
5742
|
])
|
|
@@ -5358,79 +5774,97 @@ files (v${artifact.current_version.version}):`);
|
|
|
5358
5774
|
}
|
|
5359
5775
|
});
|
|
5360
5776
|
withCommon(
|
|
5361
|
-
artifactsCmd.command("attach <ref> <name>").description(
|
|
5777
|
+
artifactsCmd.command("attach <ref> <name> [source]").description(
|
|
5362
5778
|
"Attach content to a named artifact slot (creates it, or appends the next version)"
|
|
5363
5779
|
).option("-f, --file <path>", "upload a file (MIME sniffed from the extension)").option(
|
|
5364
5780
|
"--folder <dir>",
|
|
5365
5781
|
"snapshot a directory tree as one version (collect locally, attach once; MIME per file sniffed)"
|
|
5366
|
-
).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--link <url>", "link: the URL to attach").option("--pr <spec>", "PR reference: owner/repo#N or a GitHub PR URL").option("--content-type <mime>", "declared MIME type (with --file or --text)").option("--filename <name>", "display filename (with --text; defaults to <name>.md)").option("--title <title>", "display title (with --link)").option("-d, --description <text>", "artifact description, shown in lists and launch prompts")
|
|
5782
|
+
).option("-t, --text <md|@file>", "inline text document: inline Markdown or @file").option("--link <url>", "link: the URL to attach").option("--pr <spec>", "PR reference: owner/repo#N or a GitHub PR URL").option("--content-type <mime>", "declared MIME type (with --file or --text)").option("--filename <name>", "display filename (with --text; defaults to <name>.md)").option("--title <title>", "display title (with --link)").option("-d, --description <text>", "artifact description, shown in lists and launch prompts").option(
|
|
5783
|
+
"--ignore-gates",
|
|
5784
|
+
"attach this type even when a transition requirement rejects it (skips inference and the pre-flight checks)"
|
|
5785
|
+
).addHelpText("after", ATTACH_SOURCE_HELP)
|
|
5367
5786
|
).action(
|
|
5368
|
-
async (ref, name2, opts) => {
|
|
5787
|
+
async (ref, name2, source, opts) => {
|
|
5369
5788
|
const api = client(opts);
|
|
5370
|
-
|
|
5371
|
-
(
|
|
5372
|
-
)
|
|
5373
|
-
|
|
5374
|
-
die(
|
|
5375
|
-
"pass exactly one content source: --file <path>, --folder <dir>, --text <md|@file>, --link <url>, or --pr <spec> (a link goes in --link; --url is the API base URL)"
|
|
5376
|
-
);
|
|
5789
|
+
try {
|
|
5790
|
+
assertOneSource(opts, source);
|
|
5791
|
+
} catch (err) {
|
|
5792
|
+
die(err instanceof Error ? err.message : String(err));
|
|
5377
5793
|
}
|
|
5378
5794
|
const issue = await resolveIssue(api, ref);
|
|
5795
|
+
const gates = gatesFor(issue, name2);
|
|
5796
|
+
const plan = planAttach(
|
|
5797
|
+
{
|
|
5798
|
+
ref: `${issue.project_name}/${issue.number}`,
|
|
5799
|
+
name: name2,
|
|
5800
|
+
positional: source,
|
|
5801
|
+
flags: opts,
|
|
5802
|
+
gates,
|
|
5803
|
+
probe: fsProbe
|
|
5804
|
+
},
|
|
5805
|
+
sniffContentType
|
|
5806
|
+
);
|
|
5807
|
+
const withDescription = opts.description !== void 0 ? { description: opts.description } : {};
|
|
5379
5808
|
let artifact;
|
|
5380
|
-
if (
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
}
|
|
5384
|
-
const files = walkFolder(opts.folder);
|
|
5385
|
-
if (files.length === 0) die(`${opts.folder} contains no files to snapshot`);
|
|
5809
|
+
if (plan.type === "folder") {
|
|
5810
|
+
const dir = plan.source.dir;
|
|
5811
|
+
const files = walkFolder(dir);
|
|
5812
|
+
if (files.length === 0) die(`${dir} contains no files to snapshot`);
|
|
5386
5813
|
artifact = await api.uploadArtifactFolder(issue.id, name2, files);
|
|
5387
5814
|
if (opts.description !== void 0) {
|
|
5388
5815
|
artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
|
|
5389
5816
|
}
|
|
5390
|
-
} else if (
|
|
5817
|
+
} else if (plan.type === "file") {
|
|
5818
|
+
const fromPath = plan.source.kind === "file-path" ? plan.source.path : null;
|
|
5391
5819
|
let bytes;
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5820
|
+
if (fromPath === null) {
|
|
5821
|
+
if (process.stdin.isTTY) die('"-" reads the file from stdin, but stdin is a terminal');
|
|
5822
|
+
bytes = readFileSync4(0);
|
|
5823
|
+
} else {
|
|
5824
|
+
try {
|
|
5825
|
+
bytes = readFileSync4(fromPath);
|
|
5826
|
+
} catch (err) {
|
|
5827
|
+
die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5828
|
+
}
|
|
5396
5829
|
}
|
|
5397
5830
|
artifact = await api.uploadArtifactFile(issue.id, name2, bytes, {
|
|
5398
|
-
filename:
|
|
5399
|
-
contentType:
|
|
5831
|
+
filename: plan.filename ?? (fromPath === null ? name2 : basename(fromPath)),
|
|
5832
|
+
contentType: plan.contentType ?? (fromPath === null ? "application/octet-stream" : sniffContentType(fromPath))
|
|
5400
5833
|
});
|
|
5401
5834
|
if (opts.description !== void 0) {
|
|
5402
5835
|
artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
|
|
5403
5836
|
}
|
|
5404
|
-
} else if (
|
|
5837
|
+
} else if (plan.type === "text") {
|
|
5838
|
+
const content = readTextSource(plan.source);
|
|
5405
5839
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5406
5840
|
type: "text",
|
|
5407
|
-
content
|
|
5408
|
-
...
|
|
5409
|
-
...
|
|
5410
|
-
...
|
|
5841
|
+
content,
|
|
5842
|
+
...plan.filename !== void 0 ? { filename: plan.filename } : {},
|
|
5843
|
+
...plan.contentType !== void 0 ? { content_type: plan.contentType } : {},
|
|
5844
|
+
...withDescription
|
|
5411
5845
|
});
|
|
5412
|
-
} else if (
|
|
5846
|
+
} else if (plan.type === "link") {
|
|
5847
|
+
const link = plan.source;
|
|
5413
5848
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5414
5849
|
type: "link",
|
|
5415
|
-
url:
|
|
5416
|
-
...
|
|
5417
|
-
...
|
|
5850
|
+
url: link.url,
|
|
5851
|
+
...link.title !== void 0 ? { title: link.title } : {},
|
|
5852
|
+
...withDescription
|
|
5418
5853
|
});
|
|
5419
5854
|
} else {
|
|
5420
|
-
const
|
|
5421
|
-
if (!parsed) {
|
|
5422
|
-
die(`--pr takes owner/repo#N or a GitHub PR URL, got "${opts.pr}"`);
|
|
5423
|
-
}
|
|
5855
|
+
const { pr } = plan.source;
|
|
5424
5856
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5425
5857
|
type: "pr",
|
|
5426
|
-
pr_repo_url:
|
|
5427
|
-
pr_number:
|
|
5428
|
-
...
|
|
5858
|
+
pr_repo_url: pr.repo_url,
|
|
5859
|
+
pr_number: pr.number,
|
|
5860
|
+
...withDescription
|
|
5429
5861
|
});
|
|
5430
5862
|
}
|
|
5431
5863
|
if (opts.json) return printJson(artifact);
|
|
5864
|
+
const { satisfies, rejects } = satisfiedBy(artifact, gates);
|
|
5865
|
+
const gateNote = satisfies.length > 0 ? `; satisfies ${satisfies.map((t) => `"${t}"`).join(", ")}` : rejects.length > 0 ? `, but does not satisfy ${rejects.map((r) => `"${r.transition}" (${r.wants})`).join(", ")}` : "";
|
|
5432
5866
|
console.log(
|
|
5433
|
-
`attached "${artifact.name}" v${artifact.current_version.version} (${
|
|
5867
|
+
`attached "${artifact.name}" v${artifact.current_version.version} (${artifactTypeLabel(artifact)}) to ${issue.project_name}/${issue.number} \u2014 fresh${gateNote}`
|
|
5434
5868
|
);
|
|
5435
5869
|
}
|
|
5436
5870
|
);
|
|
@@ -5476,7 +5910,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5476
5910
|
if (opts.out === void 0) {
|
|
5477
5911
|
die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
|
|
5478
5912
|
}
|
|
5479
|
-
if (
|
|
5913
|
+
if (existsSync3(opts.out) && !statSync2(opts.out).isDirectory()) {
|
|
5480
5914
|
die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
|
|
5481
5915
|
}
|
|
5482
5916
|
const files = version.files ?? [];
|
|
@@ -5499,7 +5933,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5499
5933
|
const bytes = Buffer.from(content.bytes);
|
|
5500
5934
|
if (opts.out !== void 0) {
|
|
5501
5935
|
let target2 = opts.out;
|
|
5502
|
-
if (
|
|
5936
|
+
if (existsSync3(target2) && statSync2(target2).isDirectory()) {
|
|
5503
5937
|
target2 = join2(target2, version.filename ?? name2);
|
|
5504
5938
|
}
|
|
5505
5939
|
writeFileSync2(target2, bytes);
|
|
@@ -6103,12 +6537,12 @@ import { hostname as hostname2 } from "node:os";
|
|
|
6103
6537
|
import { spawn as spawn2 } from "node:child_process";
|
|
6104
6538
|
import {
|
|
6105
6539
|
createWriteStream,
|
|
6106
|
-
existsSync as
|
|
6540
|
+
existsSync as existsSync5,
|
|
6107
6541
|
mkdirSync as mkdirSync4,
|
|
6108
6542
|
readFileSync as readFileSync9,
|
|
6109
6543
|
realpathSync,
|
|
6110
6544
|
rmSync as rmSync2,
|
|
6111
|
-
statSync as
|
|
6545
|
+
statSync as statSync4,
|
|
6112
6546
|
unlinkSync as unlinkSync2,
|
|
6113
6547
|
writeFileSync as writeFileSync3
|
|
6114
6548
|
} from "node:fs";
|
|
@@ -6224,7 +6658,7 @@ var ClaudeStreamRenderer = class {
|
|
|
6224
6658
|
|
|
6225
6659
|
// src/daemon/cli-refresh.ts
|
|
6226
6660
|
import { spawn } from "node:child_process";
|
|
6227
|
-
import { existsSync as
|
|
6661
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
6228
6662
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
6229
6663
|
|
|
6230
6664
|
// src/daemon/support.ts
|
|
@@ -6550,7 +6984,7 @@ function installedVersion(prefix) {
|
|
|
6550
6984
|
}
|
|
6551
6985
|
function lastGood(prefix) {
|
|
6552
6986
|
const binDir = binDirOf(prefix);
|
|
6553
|
-
if (!
|
|
6987
|
+
if (!existsSync4(join3(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
6554
6988
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
6555
6989
|
}
|
|
6556
6990
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
@@ -6596,7 +7030,7 @@ async function installAgentCli(opts) {
|
|
|
6596
7030
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
6597
7031
|
if (result.spawnError?.code === "ENOENT") {
|
|
6598
7032
|
const sibling = join3(dirname3(process.execPath), "npm");
|
|
6599
|
-
if (
|
|
7033
|
+
if (existsSync4(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
6600
7034
|
}
|
|
6601
7035
|
if (result.code !== 0) {
|
|
6602
7036
|
const fallback = lastGood(prefix);
|
|
@@ -6618,7 +7052,7 @@ function message(err) {
|
|
|
6618
7052
|
}
|
|
6619
7053
|
|
|
6620
7054
|
// src/daemon/store.ts
|
|
6621
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as
|
|
7055
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync3 } from "node:fs";
|
|
6622
7056
|
import { join as join4 } from "node:path";
|
|
6623
7057
|
function credentialsKey(url, name2) {
|
|
6624
7058
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
@@ -6739,7 +7173,7 @@ function directorySizeBytes(path2) {
|
|
|
6739
7173
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6740
7174
|
else if (entry.isFile()) {
|
|
6741
7175
|
try {
|
|
6742
|
-
total +=
|
|
7176
|
+
total += statSync3(child).size;
|
|
6743
7177
|
} catch {
|
|
6744
7178
|
}
|
|
6745
7179
|
}
|
|
@@ -6756,7 +7190,7 @@ async function uploadRawLog(run) {
|
|
|
6756
7190
|
run.rawSpoolPath = void 0;
|
|
6757
7191
|
try {
|
|
6758
7192
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
6759
|
-
const size =
|
|
7193
|
+
const size = statSync4(path2).size;
|
|
6760
7194
|
if (size > 0) {
|
|
6761
7195
|
let body = readFileSync9(path2);
|
|
6762
7196
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
@@ -6819,7 +7253,7 @@ async function runDaemon(opts) {
|
|
|
6819
7253
|
rmSync2(workspace, { recursive: true, force: true });
|
|
6820
7254
|
return;
|
|
6821
7255
|
}
|
|
6822
|
-
if (!
|
|
7256
|
+
if (!existsSync5(workspace)) return;
|
|
6823
7257
|
try {
|
|
6824
7258
|
writeKeptMarker(workspace, { ...marker, kept_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6825
7259
|
log(`run ${marker.run_id}: workspace kept at ${workspace}`);
|
|
@@ -6852,6 +7286,9 @@ async function runDaemon(opts) {
|
|
|
6852
7286
|
creds = { runner_id: registered.runner.id, token: registered.runner_token };
|
|
6853
7287
|
saveRunnerCredentials(opts.configDir, opts.url, opts.name, creds);
|
|
6854
7288
|
log(`registered runner "${opts.name}" (${creds.runner_id}); token stored in ${opts.configDir}`);
|
|
7289
|
+
log(
|
|
7290
|
+
`next: route work to "${opts.name}" and turn automation on \u2014 ${opts.url}/agents (or: tines routing set ${opts.name} && tines supervisor enable)`
|
|
7291
|
+
);
|
|
6855
7292
|
}
|
|
6856
7293
|
const client2 = createApiClient({ baseUrl: opts.url, apiKey: creds.token });
|
|
6857
7294
|
const statePath = daemonStatePath(opts.configDir, creds.runner_id);
|
|
@@ -7440,7 +7877,10 @@ function register8(program3) {
|
|
|
7440
7877
|
withCommon(
|
|
7441
7878
|
runnerCmd.command("daemon").description(
|
|
7442
7879
|
"Run the local runner daemon: register/reconnect, poll for assigned runs, execute them"
|
|
7443
|
-
).option(
|
|
7880
|
+
).option(
|
|
7881
|
+
"--name <name>",
|
|
7882
|
+
"runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
|
|
7883
|
+
).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
7444
7884
|
"--command <template>",
|
|
7445
7885
|
"custom harness command template ({prompt_file}, {workspace}, {model})"
|
|
7446
7886
|
).option("--max-concurrent <n>", "maximum simultaneous runs", (v) => Number.parseInt(v, 10), 1).option(
|