tines 0.0.140 → 0.0.141
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 +486 -61
- 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) => {
|
|
@@ -5259,7 +5662,7 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
5259
5662
|
`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
5663
|
);
|
|
5261
5664
|
}
|
|
5262
|
-
if (
|
|
5665
|
+
if (existsSync3(opts.out) && readdirSync(opts.out).length > 0 && !opts.force) {
|
|
5263
5666
|
die(`refusing to write into non-empty directory ${opts.out} (pass --force to override)`);
|
|
5264
5667
|
}
|
|
5265
5668
|
mkdirSync2(opts.out, { recursive: true });
|
|
@@ -5314,13 +5717,17 @@ skills: ${context.skills.map((s) => s.name).join(", ")}`);
|
|
|
5314
5717
|
const res = await api.listArtifacts(issue.id);
|
|
5315
5718
|
if (opts.json) return printJson(res);
|
|
5316
5719
|
if (res.items.length === 0) return console.log("no artifacts attached");
|
|
5720
|
+
const gates = gatesFor(issue);
|
|
5721
|
+
const labels = res.items.map((a) => gateLabel(a, gates));
|
|
5722
|
+
const gated = labels.some((l) => l !== "");
|
|
5317
5723
|
table([
|
|
5318
|
-
["NAME", "TYPE", "VERSION", "FRESH", "SUMMARY", "ATTACHED"],
|
|
5319
|
-
...res.items.map((a) => [
|
|
5724
|
+
["NAME", "TYPE", "VERSION", "FRESH", ...gated ? ["GATE"] : [], "SUMMARY", "ATTACHED"],
|
|
5725
|
+
...res.items.map((a, i) => [
|
|
5320
5726
|
a.name,
|
|
5321
5727
|
a.artifact_type,
|
|
5322
5728
|
`v${a.current_version.version}`,
|
|
5323
5729
|
a.fresh ? "yes" : "no",
|
|
5730
|
+
...gated ? [labels[i] || "ok"] : [],
|
|
5324
5731
|
artifactSummary(a),
|
|
5325
5732
|
timestamp(a.current_version.created_at)
|
|
5326
5733
|
])
|
|
@@ -5358,79 +5765,97 @@ files (v${artifact.current_version.version}):`);
|
|
|
5358
5765
|
}
|
|
5359
5766
|
});
|
|
5360
5767
|
withCommon(
|
|
5361
|
-
artifactsCmd.command("attach <ref> <name>").description(
|
|
5768
|
+
artifactsCmd.command("attach <ref> <name> [source]").description(
|
|
5362
5769
|
"Attach content to a named artifact slot (creates it, or appends the next version)"
|
|
5363
5770
|
).option("-f, --file <path>", "upload a file (MIME sniffed from the extension)").option(
|
|
5364
5771
|
"--folder <dir>",
|
|
5365
5772
|
"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")
|
|
5773
|
+
).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(
|
|
5774
|
+
"--ignore-gates",
|
|
5775
|
+
"attach this type even when a transition requirement rejects it (skips inference and the pre-flight checks)"
|
|
5776
|
+
).addHelpText("after", ATTACH_SOURCE_HELP)
|
|
5367
5777
|
).action(
|
|
5368
|
-
async (ref, name2, opts) => {
|
|
5778
|
+
async (ref, name2, source, opts) => {
|
|
5369
5779
|
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
|
-
);
|
|
5780
|
+
try {
|
|
5781
|
+
assertOneSource(opts, source);
|
|
5782
|
+
} catch (err) {
|
|
5783
|
+
die(err instanceof Error ? err.message : String(err));
|
|
5377
5784
|
}
|
|
5378
5785
|
const issue = await resolveIssue(api, ref);
|
|
5786
|
+
const gates = gatesFor(issue, name2);
|
|
5787
|
+
const plan = planAttach(
|
|
5788
|
+
{
|
|
5789
|
+
ref: `${issue.project_name}/${issue.number}`,
|
|
5790
|
+
name: name2,
|
|
5791
|
+
positional: source,
|
|
5792
|
+
flags: opts,
|
|
5793
|
+
gates,
|
|
5794
|
+
probe: fsProbe
|
|
5795
|
+
},
|
|
5796
|
+
sniffContentType
|
|
5797
|
+
);
|
|
5798
|
+
const withDescription = opts.description !== void 0 ? { description: opts.description } : {};
|
|
5379
5799
|
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`);
|
|
5800
|
+
if (plan.type === "folder") {
|
|
5801
|
+
const dir = plan.source.dir;
|
|
5802
|
+
const files = walkFolder(dir);
|
|
5803
|
+
if (files.length === 0) die(`${dir} contains no files to snapshot`);
|
|
5386
5804
|
artifact = await api.uploadArtifactFolder(issue.id, name2, files);
|
|
5387
5805
|
if (opts.description !== void 0) {
|
|
5388
5806
|
artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
|
|
5389
5807
|
}
|
|
5390
|
-
} else if (
|
|
5808
|
+
} else if (plan.type === "file") {
|
|
5809
|
+
const fromPath = plan.source.kind === "file-path" ? plan.source.path : null;
|
|
5391
5810
|
let bytes;
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5811
|
+
if (fromPath === null) {
|
|
5812
|
+
if (process.stdin.isTTY) die('"-" reads the file from stdin, but stdin is a terminal');
|
|
5813
|
+
bytes = readFileSync4(0);
|
|
5814
|
+
} else {
|
|
5815
|
+
try {
|
|
5816
|
+
bytes = readFileSync4(fromPath);
|
|
5817
|
+
} catch (err) {
|
|
5818
|
+
die(`cannot read ${fromPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5819
|
+
}
|
|
5396
5820
|
}
|
|
5397
5821
|
artifact = await api.uploadArtifactFile(issue.id, name2, bytes, {
|
|
5398
|
-
filename:
|
|
5399
|
-
contentType:
|
|
5822
|
+
filename: plan.filename ?? (fromPath === null ? name2 : basename(fromPath)),
|
|
5823
|
+
contentType: plan.contentType ?? (fromPath === null ? "application/octet-stream" : sniffContentType(fromPath))
|
|
5400
5824
|
});
|
|
5401
5825
|
if (opts.description !== void 0) {
|
|
5402
5826
|
artifact = await api.putArtifact(issue.id, name2, { description: opts.description });
|
|
5403
5827
|
}
|
|
5404
|
-
} else if (
|
|
5828
|
+
} else if (plan.type === "text") {
|
|
5829
|
+
const content = readTextSource(plan.source);
|
|
5405
5830
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5406
5831
|
type: "text",
|
|
5407
|
-
content
|
|
5408
|
-
...
|
|
5409
|
-
...
|
|
5410
|
-
...
|
|
5832
|
+
content,
|
|
5833
|
+
...plan.filename !== void 0 ? { filename: plan.filename } : {},
|
|
5834
|
+
...plan.contentType !== void 0 ? { content_type: plan.contentType } : {},
|
|
5835
|
+
...withDescription
|
|
5411
5836
|
});
|
|
5412
|
-
} else if (
|
|
5837
|
+
} else if (plan.type === "link") {
|
|
5838
|
+
const link = plan.source;
|
|
5413
5839
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5414
5840
|
type: "link",
|
|
5415
|
-
url:
|
|
5416
|
-
...
|
|
5417
|
-
...
|
|
5841
|
+
url: link.url,
|
|
5842
|
+
...link.title !== void 0 ? { title: link.title } : {},
|
|
5843
|
+
...withDescription
|
|
5418
5844
|
});
|
|
5419
5845
|
} else {
|
|
5420
|
-
const
|
|
5421
|
-
if (!parsed) {
|
|
5422
|
-
die(`--pr takes owner/repo#N or a GitHub PR URL, got "${opts.pr}"`);
|
|
5423
|
-
}
|
|
5846
|
+
const { pr } = plan.source;
|
|
5424
5847
|
artifact = await api.putArtifact(issue.id, name2, {
|
|
5425
5848
|
type: "pr",
|
|
5426
|
-
pr_repo_url:
|
|
5427
|
-
pr_number:
|
|
5428
|
-
...
|
|
5849
|
+
pr_repo_url: pr.repo_url,
|
|
5850
|
+
pr_number: pr.number,
|
|
5851
|
+
...withDescription
|
|
5429
5852
|
});
|
|
5430
5853
|
}
|
|
5431
5854
|
if (opts.json) return printJson(artifact);
|
|
5855
|
+
const { satisfies, rejects } = satisfiedBy(artifact, gates);
|
|
5856
|
+
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
5857
|
console.log(
|
|
5433
|
-
`attached "${artifact.name}" v${artifact.current_version.version} (${
|
|
5858
|
+
`attached "${artifact.name}" v${artifact.current_version.version} (${artifactTypeLabel(artifact)}) to ${issue.project_name}/${issue.number} \u2014 fresh${gateNote}`
|
|
5434
5859
|
);
|
|
5435
5860
|
}
|
|
5436
5861
|
);
|
|
@@ -5476,7 +5901,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5476
5901
|
if (opts.out === void 0) {
|
|
5477
5902
|
die(`artifact "${name2}" is a folder \u2014 pass --out <dir> to write its tree`);
|
|
5478
5903
|
}
|
|
5479
|
-
if (
|
|
5904
|
+
if (existsSync3(opts.out) && !statSync2(opts.out).isDirectory()) {
|
|
5480
5905
|
die(`--out for a folder must be a directory, and "${opts.out}" is a file`);
|
|
5481
5906
|
}
|
|
5482
5907
|
const files = version.files ?? [];
|
|
@@ -5499,7 +5924,7 @@ files (v${artifact.current_version.version}):`);
|
|
|
5499
5924
|
const bytes = Buffer.from(content.bytes);
|
|
5500
5925
|
if (opts.out !== void 0) {
|
|
5501
5926
|
let target2 = opts.out;
|
|
5502
|
-
if (
|
|
5927
|
+
if (existsSync3(target2) && statSync2(target2).isDirectory()) {
|
|
5503
5928
|
target2 = join2(target2, version.filename ?? name2);
|
|
5504
5929
|
}
|
|
5505
5930
|
writeFileSync2(target2, bytes);
|
|
@@ -6103,12 +6528,12 @@ import { hostname as hostname2 } from "node:os";
|
|
|
6103
6528
|
import { spawn as spawn2 } from "node:child_process";
|
|
6104
6529
|
import {
|
|
6105
6530
|
createWriteStream,
|
|
6106
|
-
existsSync as
|
|
6531
|
+
existsSync as existsSync5,
|
|
6107
6532
|
mkdirSync as mkdirSync4,
|
|
6108
6533
|
readFileSync as readFileSync9,
|
|
6109
6534
|
realpathSync,
|
|
6110
6535
|
rmSync as rmSync2,
|
|
6111
|
-
statSync as
|
|
6536
|
+
statSync as statSync4,
|
|
6112
6537
|
unlinkSync as unlinkSync2,
|
|
6113
6538
|
writeFileSync as writeFileSync3
|
|
6114
6539
|
} from "node:fs";
|
|
@@ -6224,7 +6649,7 @@ var ClaudeStreamRenderer = class {
|
|
|
6224
6649
|
|
|
6225
6650
|
// src/daemon/cli-refresh.ts
|
|
6226
6651
|
import { spawn } from "node:child_process";
|
|
6227
|
-
import { existsSync as
|
|
6652
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7 } from "node:fs";
|
|
6228
6653
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
6229
6654
|
|
|
6230
6655
|
// src/daemon/support.ts
|
|
@@ -6550,7 +6975,7 @@ function installedVersion(prefix) {
|
|
|
6550
6975
|
}
|
|
6551
6976
|
function lastGood(prefix) {
|
|
6552
6977
|
const binDir = binDirOf(prefix);
|
|
6553
|
-
if (!
|
|
6978
|
+
if (!existsSync4(join3(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
6554
6979
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
6555
6980
|
}
|
|
6556
6981
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
@@ -6596,7 +7021,7 @@ async function installAgentCli(opts) {
|
|
|
6596
7021
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
6597
7022
|
if (result.spawnError?.code === "ENOENT") {
|
|
6598
7023
|
const sibling = join3(dirname3(process.execPath), "npm");
|
|
6599
|
-
if (
|
|
7024
|
+
if (existsSync4(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
6600
7025
|
}
|
|
6601
7026
|
if (result.code !== 0) {
|
|
6602
7027
|
const fallback = lastGood(prefix);
|
|
@@ -6618,7 +7043,7 @@ function message(err) {
|
|
|
6618
7043
|
}
|
|
6619
7044
|
|
|
6620
7045
|
// src/daemon/store.ts
|
|
6621
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as
|
|
7046
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync3 } from "node:fs";
|
|
6622
7047
|
import { join as join4 } from "node:path";
|
|
6623
7048
|
function credentialsKey(url, name2) {
|
|
6624
7049
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
@@ -6739,7 +7164,7 @@ function directorySizeBytes(path2) {
|
|
|
6739
7164
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
6740
7165
|
else if (entry.isFile()) {
|
|
6741
7166
|
try {
|
|
6742
|
-
total +=
|
|
7167
|
+
total += statSync3(child).size;
|
|
6743
7168
|
} catch {
|
|
6744
7169
|
}
|
|
6745
7170
|
}
|
|
@@ -6756,7 +7181,7 @@ async function uploadRawLog(run) {
|
|
|
6756
7181
|
run.rawSpoolPath = void 0;
|
|
6757
7182
|
try {
|
|
6758
7183
|
await new Promise((resolve) => run.rawSpool?.end(resolve) ?? resolve());
|
|
6759
|
-
const size =
|
|
7184
|
+
const size = statSync4(path2).size;
|
|
6760
7185
|
if (size > 0) {
|
|
6761
7186
|
let body = readFileSync9(path2);
|
|
6762
7187
|
if (body.byteLength > RUN_LOG_RAW_MAX_BYTES) {
|
|
@@ -6819,7 +7244,7 @@ async function runDaemon(opts) {
|
|
|
6819
7244
|
rmSync2(workspace, { recursive: true, force: true });
|
|
6820
7245
|
return;
|
|
6821
7246
|
}
|
|
6822
|
-
if (!
|
|
7247
|
+
if (!existsSync5(workspace)) return;
|
|
6823
7248
|
try {
|
|
6824
7249
|
writeKeptMarker(workspace, { ...marker, kept_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6825
7250
|
log(`run ${marker.run_id}: workspace kept at ${workspace}`);
|