tines 0.0.173 → 0.0.174
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 +4 -0
- package/dist/index.js +251 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@ tines issues list --project <project>
|
|
|
37
37
|
tines issues create <project> --title "…" -d @description.md
|
|
38
38
|
tines issues show <project>/<number>
|
|
39
39
|
tines issues move <project>/<number> <action> # a workflow transition
|
|
40
|
+
tines issues transfer <project>/<number> --project <dest> # move to another project (keeps ID, record and old refs)
|
|
41
|
+
tines issues transfer <ref> --project <dest> --dry-run # review only: no number allocated, nothing written
|
|
42
|
+
tines issues transfer <ref> --project <dest> --inspect 0 # print reviewed guidance item [0] in full
|
|
43
|
+
tines issues transfer <ref> --project <dest> --yes # skip the prompt (still commits only the fetched review)
|
|
40
44
|
tines issues comment <project>/<number> - <<'EOF' # body from stdin; @file also works
|
|
41
45
|
…
|
|
42
46
|
EOF
|
package/dist/index.js
CHANGED
|
@@ -3605,6 +3605,14 @@ var DESCRIBERS = {
|
|
|
3605
3605
|
}
|
|
3606
3606
|
return segs;
|
|
3607
3607
|
},
|
|
3608
|
+
"issue.transferred": (_ev, p) => [
|
|
3609
|
+
text("transferred"),
|
|
3610
|
+
selfRef(),
|
|
3611
|
+
text("from"),
|
|
3612
|
+
name(p.old_ref),
|
|
3613
|
+
text("to"),
|
|
3614
|
+
name(p.new_ref)
|
|
3615
|
+
],
|
|
3608
3616
|
"issue.transitioned": (_ev, p) => {
|
|
3609
3617
|
const segs = [text("moved"), selfRef()];
|
|
3610
3618
|
if (p.action) segs.push(text("via"), name(p.action));
|
|
@@ -4036,6 +4044,15 @@ function createApiClient(options) {
|
|
|
4036
4044
|
getIssueJournal: (issueId) => get(`/api/v1/issues/${issueId}/journal`),
|
|
4037
4045
|
/** Launch prompt: stitched context plus the generated issue block. */
|
|
4038
4046
|
getIssuePrompt: (issueId) => get(`/api/v1/issues/${issueId}/prompt`),
|
|
4047
|
+
/**
|
|
4048
|
+
* Review a move to another project: read-only, allocates no number and
|
|
4049
|
+
* writes nothing. Returns the token that binds this exact review.
|
|
4050
|
+
*/
|
|
4051
|
+
previewIssueTransfer: (issueId, destinationProjectId) => get(
|
|
4052
|
+
`/api/v1/issues/${issueId}/transfer${query({ project: destinationProjectId })}`
|
|
4053
|
+
),
|
|
4054
|
+
/** Commit the reviewed move. The token must come from a fresh preview. */
|
|
4055
|
+
transferIssue: (issueId, body) => request("POST", `/api/v1/issues/${issueId}/transfer`, body),
|
|
4039
4056
|
// Issue artifacts (name-addressed under the issue)
|
|
4040
4057
|
listArtifacts: (issueId) => get(`/api/v1/issues/${issueId}/artifacts`),
|
|
4041
4058
|
getArtifact: (issueId, name2) => get(artifactPath(issueId, name2)),
|
|
@@ -4727,7 +4744,7 @@ function table(rows) {
|
|
|
4727
4744
|
console.log(formatTable(rows));
|
|
4728
4745
|
}
|
|
4729
4746
|
async function resolveProject(api, ref) {
|
|
4730
|
-
const
|
|
4747
|
+
const items = await listAll((page) => api.listProjects({ ...page, archived: "all" }));
|
|
4731
4748
|
const byId = items.find((p) => p.id === ref);
|
|
4732
4749
|
if (byId) return byId;
|
|
4733
4750
|
const byName = items.filter((p) => p.name === ref);
|
|
@@ -5023,6 +5040,149 @@ function register(program3) {
|
|
|
5023
5040
|
// src/commands/issues.ts
|
|
5024
5041
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5025
5042
|
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
5043
|
+
import { createInterface } from "node:readline/promises";
|
|
5044
|
+
|
|
5045
|
+
// src/issue-transfer.ts
|
|
5046
|
+
var CHANGE_LABEL = {
|
|
5047
|
+
added: "added by destination",
|
|
5048
|
+
removed: "removed with source",
|
|
5049
|
+
retained: "retained",
|
|
5050
|
+
rescoped: "moves with the issue",
|
|
5051
|
+
replaced: "overridden at destination"
|
|
5052
|
+
};
|
|
5053
|
+
function inspectableChanges(preview) {
|
|
5054
|
+
return preview.context.changes.filter((c) => c.change !== "retained");
|
|
5055
|
+
}
|
|
5056
|
+
function repoLine(change) {
|
|
5057
|
+
if (change.kind !== "repo") return null;
|
|
5058
|
+
const before = change.repo_before;
|
|
5059
|
+
const after = change.repo_after;
|
|
5060
|
+
const show = (r) => r ? `${r.url}${r.branch ? `#${r.branch}` : ""} \u2192 ${r.dir}` : "none";
|
|
5061
|
+
if (!before && !after) return null;
|
|
5062
|
+
return ` checkout: ${show(before)} => ${show(after)}`;
|
|
5063
|
+
}
|
|
5064
|
+
function routingLines(preview) {
|
|
5065
|
+
const lines = ["", "Routing:"];
|
|
5066
|
+
for (const [label, explainer] of [
|
|
5067
|
+
[" before", preview.routing.before],
|
|
5068
|
+
[" after ", preview.routing.after]
|
|
5069
|
+
]) {
|
|
5070
|
+
if (!explainer) {
|
|
5071
|
+
lines.push(`${label}: no explanation available`);
|
|
5072
|
+
continue;
|
|
5073
|
+
}
|
|
5074
|
+
lines.push(`${label}: ${explainer.verdict}`);
|
|
5075
|
+
if (explainer.pin) {
|
|
5076
|
+
lines.push(
|
|
5077
|
+
` pinned runner ${explainer.pin.runner_name ?? explainer.pin.runner_id}${explainer.pin.tier ? ` (${explainer.pin.tier})` : ""} \u2014 retained by the move`
|
|
5078
|
+
);
|
|
5079
|
+
}
|
|
5080
|
+
for (const tie of explainer.ambiguous_rules) {
|
|
5081
|
+
lines.push(` tied rule ${tie.scope_label}`);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
lines.push(" Capacity, heartbeats and spending are advisory and may change at any moment.");
|
|
5085
|
+
return lines;
|
|
5086
|
+
}
|
|
5087
|
+
function formatTransferPreview(preview) {
|
|
5088
|
+
const p = preview.preserved;
|
|
5089
|
+
const lines = [
|
|
5090
|
+
`Move ${preview.old_ref.ref} from ${preview.source.name} to ${preview.destination.name}`,
|
|
5091
|
+
` ${preview.number_notice} The issue keeps its stable ID (${preview.issue_id}) and every old ref keeps working.`,
|
|
5092
|
+
"",
|
|
5093
|
+
"Preserved:",
|
|
5094
|
+
` "${p.title}" \u2014 state unchanged, entered ${new Date(p.state_entered_at).toISOString()}`,
|
|
5095
|
+
` ${p.comment_count} comment(s), ${p.artifact_count} artifact(s) in ${p.artifact_version_count} version(s), ${p.run_count} run(s), ${p.link_count} link(s)`,
|
|
5096
|
+
` labels: ${p.labels.map((l) => l.name).join(", ") || "none"}`,
|
|
5097
|
+
` pins: ${p.pinned_runner_id ?? "none"}${p.pinned_tier ? ` (${p.pinned_tier})` : ""}; attempts ${p.attempt_count}${p.parked ? "; parked" : ""}`
|
|
5098
|
+
];
|
|
5099
|
+
const changes = inspectableChanges(preview);
|
|
5100
|
+
lines.push("", changes.length ? "Guidance changes:" : "Guidance: unchanged by this move.");
|
|
5101
|
+
changes.forEach((change, index) => {
|
|
5102
|
+
lines.push(` [${index}] ${change.kind} "${change.name}" \u2014 ${CHANGE_LABEL[change.change]}`);
|
|
5103
|
+
const scopeBefore = change.scope_before?.label ?? "\u2014";
|
|
5104
|
+
const scopeAfter = change.scope_after?.label ?? "\u2014";
|
|
5105
|
+
lines.push(` scope: ${scopeBefore} => ${scopeAfter}`);
|
|
5106
|
+
const repo = repoLine(change);
|
|
5107
|
+
if (repo) lines.push(repo);
|
|
5108
|
+
});
|
|
5109
|
+
for (const conflict of preview.context.after.conflicts) {
|
|
5110
|
+
lines.push(
|
|
5111
|
+
` ! ${conflict.item_ids.length} repositories still want the "${conflict.dir}" directory at the destination`
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
lines.push(...routingLines(preview));
|
|
5115
|
+
if (preview.schedule) {
|
|
5116
|
+
lines.push(
|
|
5117
|
+
"",
|
|
5118
|
+
`Schedule: ${preview.schedule.name} (${preview.schedule.project_name}) \u2014 ${preview.schedule.notice}`
|
|
5119
|
+
);
|
|
5120
|
+
}
|
|
5121
|
+
if (preview.blockers.length) {
|
|
5122
|
+
lines.push("", "Cannot move yet:");
|
|
5123
|
+
for (const blocker of preview.blockers) {
|
|
5124
|
+
lines.push(
|
|
5125
|
+
` ${blocker.code}: ${blocker.message}${blocker.remedy ? ` (${blocker.remedy})` : ""}`
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
if (preview.noop)
|
|
5130
|
+
lines.push("", "This issue is already in that project; moving would change nothing.");
|
|
5131
|
+
return lines.join("\n");
|
|
5132
|
+
}
|
|
5133
|
+
function formatTransferItem(preview, index) {
|
|
5134
|
+
const change = inspectableChanges(preview)[index];
|
|
5135
|
+
if (!change) return `no item [${index}] in this review`;
|
|
5136
|
+
const side = change.change === "removed" ? preview.context.before : preview.context.after;
|
|
5137
|
+
const prompt = side.prompt.parts.find((part) => part.item_id === change.item_id);
|
|
5138
|
+
if (prompt) return `${change.name} (prompt, ${prompt.scope.label})
|
|
5139
|
+
${prompt.body}`;
|
|
5140
|
+
const skill = side.skills.find((s) => s.item_id === change.item_id);
|
|
5141
|
+
if (skill) {
|
|
5142
|
+
return [
|
|
5143
|
+
`${change.name} (skill, ${skill.scope.label})`,
|
|
5144
|
+
...skill.files.map((f) => `--- ${f.path}
|
|
5145
|
+
${f.content}`)
|
|
5146
|
+
].join("\n");
|
|
5147
|
+
}
|
|
5148
|
+
const repo = side.repos.find((r) => r.item_id === change.item_id);
|
|
5149
|
+
if (repo)
|
|
5150
|
+
return `${change.name} (repo, ${repo.scope.label})
|
|
5151
|
+
${repo.url} ${repo.branch ?? ""} \u2192 ${repo.dir}`;
|
|
5152
|
+
return `${change.name} (${change.kind}) is not effective on either side; nothing to show.`;
|
|
5153
|
+
}
|
|
5154
|
+
function formatTransferResult(result) {
|
|
5155
|
+
if (result.status === "noop") {
|
|
5156
|
+
return `${result.old_ref.ref} is already in ${result.destination.name}; nothing changed`;
|
|
5157
|
+
}
|
|
5158
|
+
return [
|
|
5159
|
+
`moved ${result.old_ref.ref} to ${result.new_ref.ref}`,
|
|
5160
|
+
` ${result.old_ref.ref} still resolves to this issue`,
|
|
5161
|
+
` ${result.new_ref.ref}`
|
|
5162
|
+
].join("\n");
|
|
5163
|
+
}
|
|
5164
|
+
async function confirmTransfer(preview, terminal, notice) {
|
|
5165
|
+
if (notice) terminal.write(`${notice}
|
|
5166
|
+
`);
|
|
5167
|
+
terminal.write(`${formatTransferPreview(preview)}
|
|
5168
|
+
`);
|
|
5169
|
+
const inspectable = inspectableChanges(preview).length;
|
|
5170
|
+
for (; ; ) {
|
|
5171
|
+
const hint = inspectable ? `, or a number 0-${inspectable - 1} to read that item` : "";
|
|
5172
|
+
const answer = await terminal.ask(
|
|
5173
|
+
`Move ${preview.old_ref.ref} to ${preview.destination.name}? [y/N${hint}] `
|
|
5174
|
+
);
|
|
5175
|
+
if (answer === null) return { action: "abort", reason: "no answer" };
|
|
5176
|
+
const trimmed = answer.trim();
|
|
5177
|
+
if (/^\d+$/.test(trimmed)) {
|
|
5178
|
+
terminal.write(`${formatTransferItem(preview, Number(trimmed))}
|
|
5179
|
+
`);
|
|
5180
|
+
continue;
|
|
5181
|
+
}
|
|
5182
|
+
if (/^y(es)?$/i.test(trimmed)) return { action: "commit" };
|
|
5183
|
+
return { action: "abort", reason: "aborted" };
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5026
5186
|
|
|
5027
5187
|
// src/attach-source.ts
|
|
5028
5188
|
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
@@ -5755,6 +5915,92 @@ function register2(program3) {
|
|
|
5755
5915
|
`${moved.project_name}/#${moved.number}: ${issue.state.name} \u2192 ${moved.state.name} ("${action2}")`
|
|
5756
5916
|
);
|
|
5757
5917
|
});
|
|
5918
|
+
withCommon(
|
|
5919
|
+
issues.command("transfer <ref>").description(
|
|
5920
|
+
'Move an issue to another project, keeping its ID, record and old refs (this is not the workflow "move")'
|
|
5921
|
+
).requiredOption("-p, --project <name-or-id>", "destination project").option("--dry-run", "print the review and exit without moving anything").option("--inspect <n>", "print the full content of reviewed guidance item [n]").option("-y, --yes", "skip the confirmation prompt")
|
|
5922
|
+
).action(
|
|
5923
|
+
async (ref, opts) => {
|
|
5924
|
+
const api = client(opts);
|
|
5925
|
+
const issue = await resolveIssue(api, ref);
|
|
5926
|
+
const destination = await resolveProject(api, opts.project);
|
|
5927
|
+
let preview = await api.previewIssueTransfer(issue.id, destination.id);
|
|
5928
|
+
if (opts.inspect !== void 0) {
|
|
5929
|
+
const text3 = formatTransferItem(preview, Number(opts.inspect));
|
|
5930
|
+
if (opts.json) return printJson({ ...preview, inspected: text3 });
|
|
5931
|
+
return console.log(text3);
|
|
5932
|
+
}
|
|
5933
|
+
if (opts.dryRun) {
|
|
5934
|
+
if (opts.json) return printJson(preview);
|
|
5935
|
+
return console.log(formatTransferPreview(preview));
|
|
5936
|
+
}
|
|
5937
|
+
const blocked = () => {
|
|
5938
|
+
const blocker = preview.blockers[0];
|
|
5939
|
+
if (opts.json) {
|
|
5940
|
+
printJson({
|
|
5941
|
+
error: {
|
|
5942
|
+
code: blocker?.code ?? "transfer_blocked",
|
|
5943
|
+
message: blocker?.message ?? "this move is blocked",
|
|
5944
|
+
details: blocker ?? null
|
|
5945
|
+
}
|
|
5946
|
+
});
|
|
5947
|
+
process.exitCode = 1;
|
|
5948
|
+
return;
|
|
5949
|
+
}
|
|
5950
|
+
console.error(formatTransferPreview(preview));
|
|
5951
|
+
return die(blocker ? `${blocker.code}: ${blocker.message}` : "this move is blocked");
|
|
5952
|
+
};
|
|
5953
|
+
if (!preview.can_commit || !preview.preview_token) return blocked();
|
|
5954
|
+
const interactive = !opts.yes && process.stdin.isTTY;
|
|
5955
|
+
if (!opts.yes && !interactive) {
|
|
5956
|
+
die(
|
|
5957
|
+
`refusing to move without a confirmation: rerun with --yes, or review it first with --dry-run --json`
|
|
5958
|
+
);
|
|
5959
|
+
}
|
|
5960
|
+
const rl = interactive ? createInterface({ input: process.stdin, output: process.stderr }) : null;
|
|
5961
|
+
const terminal = {
|
|
5962
|
+
write: (text3) => process.stderr.write(text3),
|
|
5963
|
+
ask: (question) => rl.question(question).catch(() => null)
|
|
5964
|
+
};
|
|
5965
|
+
try {
|
|
5966
|
+
for (let attempt = 0; ; attempt++) {
|
|
5967
|
+
if (interactive) {
|
|
5968
|
+
const decision = await confirmTransfer(
|
|
5969
|
+
preview,
|
|
5970
|
+
terminal,
|
|
5971
|
+
attempt === 0 ? void 0 : "The configuration this review described has changed. Here is the current one:"
|
|
5972
|
+
);
|
|
5973
|
+
if (decision.action === "abort") return die(decision.reason);
|
|
5974
|
+
}
|
|
5975
|
+
try {
|
|
5976
|
+
const result = await api.transferIssue(issue.id, {
|
|
5977
|
+
project_id: destination.id,
|
|
5978
|
+
preview_token: preview.preview_token
|
|
5979
|
+
});
|
|
5980
|
+
if (opts.json) return printJson(result);
|
|
5981
|
+
return console.log(formatTransferResult(result));
|
|
5982
|
+
} catch (e) {
|
|
5983
|
+
const fail = e;
|
|
5984
|
+
if (fail.code === "transfer_preview_stale" && interactive && attempt === 0) {
|
|
5985
|
+
preview = await api.previewIssueTransfer(issue.id, destination.id);
|
|
5986
|
+
if (!preview.can_commit || !preview.preview_token) return blocked();
|
|
5987
|
+
continue;
|
|
5988
|
+
}
|
|
5989
|
+
if (opts.json && fail.code) {
|
|
5990
|
+
printJson({
|
|
5991
|
+
error: { code: fail.code, message: fail.message, details: fail.details ?? null }
|
|
5992
|
+
});
|
|
5993
|
+
process.exitCode = 1;
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
throw e;
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
} finally {
|
|
6000
|
+
rl?.close();
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
);
|
|
5758
6004
|
withCommon(
|
|
5759
6005
|
issues.command("comment <ref> <markdown>").description(`Comment on an issue \u2014 Markdown body: ${BODY_VALUE_HELP}`).passThroughOptions()
|
|
5760
6006
|
).action(async (ref, markdown, opts, command) => {
|
|
@@ -6437,7 +6683,7 @@ start one: tines journal append ${issue.project_name}/${issue.number} "- <date>:
|
|
|
6437
6683
|
}
|
|
6438
6684
|
|
|
6439
6685
|
// src/commands/labels.ts
|
|
6440
|
-
import { createInterface } from "node:readline/promises";
|
|
6686
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
6441
6687
|
function parseLabelColor(value) {
|
|
6442
6688
|
if (value === void 0) return void 0;
|
|
6443
6689
|
if (!LABEL_COLORS.includes(value)) {
|
|
@@ -6510,7 +6756,7 @@ function register4(program3) {
|
|
|
6510
6756
|
`${existing.routing_rule_count} routing rule${existing.routing_rule_count === 1 ? "" : "s"}`
|
|
6511
6757
|
] : []
|
|
6512
6758
|
].join(", ");
|
|
6513
|
-
const rl =
|
|
6759
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
6514
6760
|
const answer = await rl.question(
|
|
6515
6761
|
`Delete label "${existing.name}"${opts.force ? " and everything scoped to it" : ""}? It is on ${scoped}. [y/N] `
|
|
6516
6762
|
);
|
|
@@ -9015,7 +9261,7 @@ function register8(program3) {
|
|
|
9015
9261
|
}
|
|
9016
9262
|
|
|
9017
9263
|
// src/commands/schedules.ts
|
|
9018
|
-
import { createInterface as
|
|
9264
|
+
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
9019
9265
|
async function resolveSchedule(api, ref) {
|
|
9020
9266
|
const { project, name: name2 } = parseScheduleRef(ref);
|
|
9021
9267
|
const proj = await resolveProject(api, project);
|
|
@@ -9179,7 +9425,7 @@ recent instances:`);
|
|
|
9179
9425
|
const api = client(opts);
|
|
9180
9426
|
const schedule = await resolveSchedule(api, ref);
|
|
9181
9427
|
if (!opts.yes) {
|
|
9182
|
-
const rl =
|
|
9428
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
9183
9429
|
const answer = await rl.question(
|
|
9184
9430
|
`Delete schedule "${scheduleRef(schedule)}"? Its ${schedule.run_count} existing issue${schedule.run_count === 1 ? "" : "s"} will be kept. [y/N] `
|
|
9185
9431
|
);
|