shipbench 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +205 -32
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4029,6 +4029,8 @@ var CONFIG_PATH2 = ".shipbench/config.json";
|
|
|
4029
4029
|
var LAYOUT_PATH2 = ".shipbench/layout.json";
|
|
4030
4030
|
var UPDATES_HEADING = "## Task Updates";
|
|
4031
4031
|
var ISO_8601_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
4032
|
+
var ATX_HEADING = /^(#{1,6})\s+(.+?)\s*$/;
|
|
4033
|
+
var ENTRY_HEADING_TEXT = /^\d{4}-\d{2}-\d{2}/;
|
|
4032
4034
|
var updatesParseWarnings = /* @__PURE__ */ new WeakMap();
|
|
4033
4035
|
var ArchiveBlockedError = class extends Error {
|
|
4034
4036
|
constructor(slug, dependentSlugs) {
|
|
@@ -4109,11 +4111,17 @@ function parseTaskBody(rawBody) {
|
|
|
4109
4111
|
const outsideFence = fence === null;
|
|
4110
4112
|
const fenceOnLine = /^\s{0,3}(`{3,}|~{3,})/.test(line);
|
|
4111
4113
|
if (outsideFence && !fenceOnLine) {
|
|
4112
|
-
const heading = line.match(
|
|
4113
|
-
if (heading) {
|
|
4114
|
+
const heading = line.match(ATX_HEADING);
|
|
4115
|
+
if (heading && ENTRY_HEADING_TEXT.test(heading[2])) {
|
|
4116
|
+
if (heading[1].length !== 3) {
|
|
4117
|
+
return malformedUpdates(
|
|
4118
|
+
body,
|
|
4119
|
+
`expected each entry heading to use "### <ISO 8601 timestamp>".`
|
|
4120
|
+
);
|
|
4121
|
+
}
|
|
4114
4122
|
const previousError = finishComment();
|
|
4115
4123
|
if (previousError) return malformedUpdates(body, previousError);
|
|
4116
|
-
const nextTimestamp = heading[
|
|
4124
|
+
const nextTimestamp = heading[2];
|
|
4117
4125
|
if (!ISO_8601_TIMESTAMP.test(nextTimestamp) || Number.isNaN(Date.parse(nextTimestamp))) {
|
|
4118
4126
|
return malformedUpdates(
|
|
4119
4127
|
body,
|
|
@@ -4124,12 +4132,6 @@ function parseTaskBody(rawBody) {
|
|
|
4124
4132
|
textLines = [];
|
|
4125
4133
|
continue;
|
|
4126
4134
|
}
|
|
4127
|
-
if (/^#{1,6}(?:\s|$)/.test(line)) {
|
|
4128
|
-
return malformedUpdates(
|
|
4129
|
-
body,
|
|
4130
|
-
`expected each entry heading to use "### <ISO 8601 timestamp>".`
|
|
4131
|
-
);
|
|
4132
|
-
}
|
|
4133
4135
|
}
|
|
4134
4136
|
if (timestamp === null) {
|
|
4135
4137
|
if (line.trim()) {
|
|
@@ -4153,6 +4155,46 @@ function parseTaskBody(rawBody) {
|
|
|
4153
4155
|
}
|
|
4154
4156
|
return { body: description, comments };
|
|
4155
4157
|
}
|
|
4158
|
+
function assertBodyWithoutUpdatesMarker(body) {
|
|
4159
|
+
let fence = null;
|
|
4160
|
+
for (const line of body.split(/\r?\n/)) {
|
|
4161
|
+
if (!fence && line.trimEnd() === UPDATES_HEADING) {
|
|
4162
|
+
throw new Error(
|
|
4163
|
+
`Invalid task description: remove the "${UPDATES_HEADING}" heading \u2014 that section is written by \`task comment\`. Put the heading in a code fence if the description means it literally.`
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
fence = updateFence(line, fence);
|
|
4167
|
+
}
|
|
4168
|
+
if (fence) {
|
|
4169
|
+
throw new Error(
|
|
4170
|
+
"Invalid task description: close the code fence this description opens. An open fence runs past the end of the description and hides the Updates section from every read."
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function assertCommentTextIsParsable(text) {
|
|
4175
|
+
let fence = null;
|
|
4176
|
+
for (const line of text.split(/\r?\n/)) {
|
|
4177
|
+
if (!fence) {
|
|
4178
|
+
if (line.trimEnd() === UPDATES_HEADING) {
|
|
4179
|
+
throw new Error(
|
|
4180
|
+
`Invalid task update: remove the "${UPDATES_HEADING}" heading \u2014 a second one would leave the section unreadable. Put it in a code fence if the update means it literally.`
|
|
4181
|
+
);
|
|
4182
|
+
}
|
|
4183
|
+
const heading = line.match(ATX_HEADING);
|
|
4184
|
+
if (heading && ENTRY_HEADING_TEXT.test(heading[2])) {
|
|
4185
|
+
throw new Error(
|
|
4186
|
+
`Invalid task update: "${line.trim()}" reads as an entry heading and would split this update in two. Indent it, fence it, or drop the leading "#".`
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
fence = updateFence(line, fence);
|
|
4191
|
+
}
|
|
4192
|
+
if (fence) {
|
|
4193
|
+
throw new Error(
|
|
4194
|
+
"Invalid task update: close the code fence this update opens. An open fence runs past the end of the update and swallows the entries below it."
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4156
4198
|
function parseFrontmatter(fileContent) {
|
|
4157
4199
|
try {
|
|
4158
4200
|
return (0, import_gray_matter.default)(fileContent);
|
|
@@ -4360,7 +4402,8 @@ function taskFileSlugs(result) {
|
|
|
4360
4402
|
])
|
|
4361
4403
|
];
|
|
4362
4404
|
}
|
|
4363
|
-
async function createTask(adapter, config, title, fields) {
|
|
4405
|
+
async function createTask(adapter, config, title, fields, body) {
|
|
4406
|
+
if (body !== void 0) assertBodyWithoutUpdatesMarker(body);
|
|
4364
4407
|
const [existingFiles, archivedFiles] = await Promise.all([
|
|
4365
4408
|
adapter.listFiles(TASKS_DIR),
|
|
4366
4409
|
adapter.listFiles(ARCHIVE_DIR)
|
|
@@ -4394,7 +4437,7 @@ async function createTask(adapter, config, title, fields) {
|
|
|
4394
4437
|
created: now,
|
|
4395
4438
|
updated: now
|
|
4396
4439
|
},
|
|
4397
|
-
body: "",
|
|
4440
|
+
body: body ?? "",
|
|
4398
4441
|
comments: []
|
|
4399
4442
|
};
|
|
4400
4443
|
await adapter.writeFile(`${TASKS_DIR}/${slug}.md`, serializeTask(task));
|
|
@@ -4408,6 +4451,7 @@ async function createTask(adapter, config, title, fields) {
|
|
|
4408
4451
|
return task;
|
|
4409
4452
|
}
|
|
4410
4453
|
async function updateTask(adapter, config, slug, fields, body) {
|
|
4454
|
+
if (body !== void 0) assertBodyWithoutUpdatesMarker(body);
|
|
4411
4455
|
const path = `${TASKS_DIR}/${slug}.md`;
|
|
4412
4456
|
const content = await adapter.readFile(path);
|
|
4413
4457
|
const task = parseTaskFile(slug, content);
|
|
@@ -4449,6 +4493,7 @@ async function addComment(adapter, config, slug, text) {
|
|
|
4449
4493
|
if (!normalizedText) {
|
|
4450
4494
|
throw new Error("Task update text must not be blank.");
|
|
4451
4495
|
}
|
|
4496
|
+
assertCommentTextIsParsable(normalizedText);
|
|
4452
4497
|
const path = `${TASKS_DIR}/${slug}.md`;
|
|
4453
4498
|
const content = await adapter.readFile(path);
|
|
4454
4499
|
const task = parseTaskFile(slug, content);
|
|
@@ -4484,6 +4529,7 @@ async function editComment(adapter, config, slug, index, text) {
|
|
|
4484
4529
|
if (!normalizedText) {
|
|
4485
4530
|
throw new Error("Task update text must not be blank.");
|
|
4486
4531
|
}
|
|
4532
|
+
assertCommentTextIsParsable(normalizedText);
|
|
4487
4533
|
const path = `${TASKS_DIR}/${slug}.md`;
|
|
4488
4534
|
const content = await adapter.readFile(path);
|
|
4489
4535
|
const task = parseTaskFile(slug, content);
|
|
@@ -4818,8 +4864,12 @@ Every file in \`tasks/\` is a Markdown document with a YAML frontmatter block. S
|
|
|
4818
4864
|
|
|
4819
4865
|
Read the narrowest thing that answers the question. Because each task has a slug, read one task when one task is enough. Use list, search, or archive reads only for broader questions.
|
|
4820
4866
|
|
|
4867
|
+
Write a description with the task instead of after it: \`shipbench task create "Task title" --body-file description.md\`, and \`shipbench task edit <slug> --body-file revised.md\` to replace one. ShipBench reads the file as UTF-8 itself, so multi-line Markdown never passes through shell quoting or a shell's encoding.
|
|
4868
|
+
|
|
4821
4869
|
Each task may end with a reserved \`## Task Updates\` section. Use it for time-anchored decisions, pivots, and external events that would lose meaning without their timestamp. Keep timeless facts in the description instead. Append with \`shipbench task comment <slug> "What changed and why."\`, edit text with \`shipbench task comment edit <slug> <index> "Corrected text."\`, or delete with \`shipbench task comment delete <slug> <index>\`. Indices are zero-based. Edits preserve the entry's timestamp; Git preserves earlier text and deleted entries.
|
|
4822
4870
|
|
|
4871
|
+
Both commands also take \`--body <text>\` and \`--body-file <path>\` in place of the positional text, and \`--body-file\` is the one to reach for when an update runs to several lines: ShipBench reads the file as UTF-8 itself, so the prose never passes through shell quoting or a shell's encoding.
|
|
4872
|
+
|
|
4823
4873
|
Archived tasks live in \`tasks/archive/\` and are excluded from normal board reads. Archiving moves the file without changing its frontmatter or timestamps; unarchiving restores the same file to \`tasks/\`.
|
|
4824
4874
|
`;
|
|
4825
4875
|
}
|
|
@@ -4890,6 +4940,12 @@ This heuristic is guidance, not a validation rule. Core stores each entry as \`{
|
|
|
4890
4940
|
|
|
4891
4941
|
Append through \`shipbench task comment <slug> "What changed and why."\`. Edit text with \`shipbench task comment edit <slug> <index> "Corrected text."\`; delete an entry with \`shipbench task comment delete <slug> <index>\`. Indices are zero-based. Editing never changes the entry's timestamp. Git preserves earlier text and deleted entries.
|
|
4892
4942
|
|
|
4943
|
+
Append and edit both accept \`--body <text>\` or \`--body-file <path>\` instead of the positional text, the same pair \`task create\` and \`task edit\` take. Use \`--body-file\` for anything multi-line: ShipBench reads the file as UTF-8, so the text never passes through shell quoting or encoding.
|
|
4944
|
+
|
|
4945
|
+
Update text is prose. Markdown headings inside it are yours to use \u2014 only a column-0 \`### <ISO 8601 timestamp>\` line opens a new entry. Three things are rejected on write, because the next read would mis-file them: a \`## Task Updates\` heading of its own, a column-0 heading whose text is a date, and an unclosed code fence.
|
|
4946
|
+
|
|
4947
|
+
A description may not contain a \`## Task Updates\` heading of its own \u2014 the next read would file part of it as entries, so ShipBench rejects the write. It also may not leave a code fence open, which would swallow the marker below it and hide every entry. Put the heading in a code fence when a description means it literally.
|
|
4948
|
+
|
|
4893
4949
|
Do not hand-edit content below the \`## Task Updates\` marker when the CLI is available.
|
|
4894
4950
|
|
|
4895
4951
|
## Choosing What to Work On
|
|
@@ -4973,9 +5029,12 @@ Prefer the ShipBench CLI for task mutations when it is available. The CLI routes
|
|
|
4973
5029
|
- **Inspect dependencies**: \`shipbench task graph --json\`
|
|
4974
5030
|
- **Include descriptions in a list**: \`shipbench task list --json --include-body\`
|
|
4975
5031
|
- **Create a task**: \`shipbench task create "Task title" --status=todo\`
|
|
5032
|
+
- **Create a task with a description**: \`shipbench task create "Task title" --body-file=description.md\` (or \`--body "One-line description."\`)
|
|
5033
|
+
- **Rewrite a description**: \`shipbench task edit <slug> --body-file=description.md\` (replaces it whole; \`--body ""\` clears it)
|
|
4976
5034
|
- **Create a dependent task**: \`shipbench task create "Task title" --depends-on=other-slug,another-slug\`
|
|
4977
5035
|
- **Add a time-anchored update**: \`shipbench task comment <slug> "What changed and why."\`
|
|
4978
|
-
- **
|
|
5036
|
+
- **Add a multi-line update**: \`shipbench task comment <slug> --body-file update.md\`
|
|
5037
|
+
- **Edit an update's text**: \`shipbench task comment edit <slug> <index> "Corrected text."\` (also takes \`--body-file\`)
|
|
4979
5038
|
- **Delete an update**: \`shipbench task comment delete <slug> <index>\`
|
|
4980
5039
|
- **Move a task**: \`shipbench task move <slug> --to=in-progress\`
|
|
4981
5040
|
- **Complete a task**: \`shipbench task move <slug> --to=done\`
|
|
@@ -4994,7 +5053,7 @@ Use direct edits only when the CLI is unavailable or when changing task descript
|
|
|
4994
5053
|
|
|
4995
5054
|
- **Create a task**: Add a new \`.md\` file in \`tasks/\` following the format above.
|
|
4996
5055
|
- **Move a task**: Change the \`status\` field and update the \`updated\` timestamp.
|
|
4997
|
-
- **Edit a task**: Modify frontmatter fields and/or the description above \`## Task Updates\`. Always update \`updated\`.
|
|
5056
|
+
- **Edit a task**: Modify frontmatter fields and/or the description above \`## Task Updates\`. Always update \`updated\`. The CLI reaches descriptions \u2014 use \`task edit\` rather than rewriting a file by hand.
|
|
4998
5057
|
- **Add an Update without the CLI**: Append a \`### <ISO 8601 timestamp>\` heading and text below the trailing \`## Task Updates\` marker.
|
|
4999
5058
|
- **Edit an Update without the CLI**: Change only its text; preserve the \`###\` timestamp heading and update the frontmatter \`updated\` value.
|
|
5000
5059
|
- **Delete an Update without the CLI**: Remove its heading and text, remove an empty \`## Task Updates\` section, and update the frontmatter \`updated\` value.
|
|
@@ -5004,7 +5063,7 @@ Use direct edits only when the CLI is unavailable or when changing task descript
|
|
|
5004
5063
|
|
|
5005
5064
|
- Never invent status values not listed in \`config.json\`.
|
|
5006
5065
|
- Reorder tasks only when the user explicitly asks for it.
|
|
5007
|
-
- Always update the \`updated\` timestamp when modifying a task.
|
|
5066
|
+
- Always update the \`updated\` timestamp when modifying a task by hand. Every CLI mutation maintains it for you.
|
|
5008
5067
|
- Do not modify \`config.json\` unless explicitly asked.
|
|
5009
5068
|
- Do not read \`layout.json\` as the visible order or modify it; the CLI and Board own this partial index.
|
|
5010
5069
|
- Do not read or modify \`tasks/archive/\` unless the user explicitly asks about archived work.
|
|
@@ -5357,6 +5416,7 @@ import { statSync } from "fs";
|
|
|
5357
5416
|
import { basename, resolve as resolve3 } from "path";
|
|
5358
5417
|
|
|
5359
5418
|
// src/cli.ts
|
|
5419
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5360
5420
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5361
5421
|
|
|
5362
5422
|
// src/boardServer.ts
|
|
@@ -6567,7 +6627,7 @@ async function runTui(options2) {
|
|
|
6567
6627
|
}
|
|
6568
6628
|
|
|
6569
6629
|
// src/cli.ts
|
|
6570
|
-
var VERSION = true ? "0.
|
|
6630
|
+
var VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
6571
6631
|
function commaList(value) {
|
|
6572
6632
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6573
6633
|
}
|
|
@@ -6656,6 +6716,33 @@ function formatTaskDependencyGraph(graph) {
|
|
|
6656
6716
|
}
|
|
6657
6717
|
return lines;
|
|
6658
6718
|
}
|
|
6719
|
+
function bodyOption() {
|
|
6720
|
+
return new Option("--body <text>", "Description as Markdown text").conflicts([
|
|
6721
|
+
"bodyFile"
|
|
6722
|
+
]);
|
|
6723
|
+
}
|
|
6724
|
+
function bodyFileOption() {
|
|
6725
|
+
return new Option(
|
|
6726
|
+
"--body-file <path>",
|
|
6727
|
+
'Read the description from a UTF-8 file; "-" reads stdin'
|
|
6728
|
+
).conflicts(["body"]);
|
|
6729
|
+
}
|
|
6730
|
+
function updateTextOption() {
|
|
6731
|
+
return new Option("--body <text>", "Update text as Markdown").conflicts([
|
|
6732
|
+
"bodyFile"
|
|
6733
|
+
]);
|
|
6734
|
+
}
|
|
6735
|
+
function updateTextFileOption() {
|
|
6736
|
+
return new Option(
|
|
6737
|
+
"--body-file <path>",
|
|
6738
|
+
'Read the update text from a UTF-8 file; "-" reads stdin'
|
|
6739
|
+
).conflicts(["body"]);
|
|
6740
|
+
}
|
|
6741
|
+
async function readProcessStdin() {
|
|
6742
|
+
const chunks = [];
|
|
6743
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
6744
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
6745
|
+
}
|
|
6659
6746
|
function enableExitOverride(command) {
|
|
6660
6747
|
command.exitOverride();
|
|
6661
6748
|
for (const child of command.commands) enableExitOverride(child);
|
|
@@ -6684,7 +6771,30 @@ function createCli(opts) {
|
|
|
6684
6771
|
});
|
|
6685
6772
|
const fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
|
|
6686
6773
|
const runGit = opts.runGit ?? runGitCommand;
|
|
6687
|
-
const
|
|
6774
|
+
const readTextFile = opts.readTextFile ?? ((path) => readFile3(path, "utf8"));
|
|
6775
|
+
const readStdin = opts.readStdin ?? readProcessStdin;
|
|
6776
|
+
const resolveBody = async (raw) => {
|
|
6777
|
+
if (raw.bodyFile === void 0) return raw.body;
|
|
6778
|
+
if (raw.bodyFile === "-") return readStdin();
|
|
6779
|
+
try {
|
|
6780
|
+
return await readTextFile(raw.bodyFile);
|
|
6781
|
+
} catch (error) {
|
|
6782
|
+
throw new Error(
|
|
6783
|
+
`Cannot read --body-file "${raw.bodyFile}": ${error instanceof Error ? error.message : String(error)}`
|
|
6784
|
+
);
|
|
6785
|
+
}
|
|
6786
|
+
};
|
|
6787
|
+
const resolveUpdateText = async (command, positional, raw) => {
|
|
6788
|
+
const fromOption = await resolveBody(raw);
|
|
6789
|
+
if (positional != null && fromOption !== void 0) {
|
|
6790
|
+
command.error(
|
|
6791
|
+
"Pass the update text once: either positionally or with --body / --body-file, not both."
|
|
6792
|
+
);
|
|
6793
|
+
return void 0;
|
|
6794
|
+
}
|
|
6795
|
+
return positional ?? fromOption;
|
|
6796
|
+
};
|
|
6797
|
+
const program = new Command().name("shipbench").description("Git-native project management for solo developers.").enablePositionalOptions().version(VERSION, "-v, --version", "output the version").option(
|
|
6688
6798
|
"-C <path>",
|
|
6689
6799
|
"Run as if ShipBench was started in the specified directory"
|
|
6690
6800
|
);
|
|
@@ -6802,15 +6912,25 @@ function createCli(opts) {
|
|
|
6802
6912
|
"Slugs this task depends on (comma-separated, repeatable)",
|
|
6803
6913
|
accumulateCommaList,
|
|
6804
6914
|
[]
|
|
6805
|
-
).option("--json", "Output the created task as JSON").
|
|
6915
|
+
).addOption(bodyOption()).addOption(bodyFileOption()).option("--json", "Output the created task as JSON").addHelpText(
|
|
6916
|
+
"after",
|
|
6917
|
+
"\nPrefer --body-file for anything multi-line: the file is read as UTF-8 by\nShipBench, so the description never passes through shell quoting or encoding.\n"
|
|
6918
|
+
).action(async (title, raw) => {
|
|
6919
|
+
const body = await resolveBody(raw);
|
|
6806
6920
|
const config = await loadCliConfig();
|
|
6807
|
-
const created = await createTask(
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6921
|
+
const created = await createTask(
|
|
6922
|
+
adapter,
|
|
6923
|
+
config,
|
|
6924
|
+
title,
|
|
6925
|
+
{
|
|
6926
|
+
status: raw.status,
|
|
6927
|
+
assignee: raw.assignee,
|
|
6928
|
+
priority: raw.priority,
|
|
6929
|
+
tags: raw.tags,
|
|
6930
|
+
depends_on: raw.dependsOn
|
|
6931
|
+
},
|
|
6932
|
+
body
|
|
6933
|
+
);
|
|
6814
6934
|
if (raw.json) {
|
|
6815
6935
|
data(
|
|
6816
6936
|
JSON.stringify(
|
|
@@ -6829,19 +6949,72 @@ function createCli(opts) {
|
|
|
6829
6949
|
}
|
|
6830
6950
|
chrome(`Created task: ${created.slug}`);
|
|
6831
6951
|
});
|
|
6832
|
-
const
|
|
6833
|
-
|
|
6952
|
+
const editCommand = task.command("edit <slug>").description("Replace a task's Markdown description").addOption(bodyOption()).addOption(bodyFileOption()).option("--json", "Output the edited task as JSON").addHelpText(
|
|
6953
|
+
"after",
|
|
6954
|
+
"\nThe description is replaced whole and an empty value clears it. The Task\nUpdates section is never touched \u2014 use `shipbench task comment` for those.\n"
|
|
6955
|
+
);
|
|
6956
|
+
editCommand.action(async (slug, raw) => {
|
|
6957
|
+
const body = await resolveBody(raw);
|
|
6958
|
+
if (body === void 0) {
|
|
6959
|
+
editCommand.error(
|
|
6960
|
+
'Provide --body <text> or --body-file <path> (use "-" to read stdin).'
|
|
6961
|
+
);
|
|
6962
|
+
return;
|
|
6963
|
+
}
|
|
6964
|
+
const config = await loadCliConfig();
|
|
6965
|
+
const existing = await getTask(adapter, config, slug);
|
|
6966
|
+
if (!existing) {
|
|
6967
|
+
const archived = await getTask(adapter, config, slug, { archived: true });
|
|
6968
|
+
editCommand.error(
|
|
6969
|
+
archived ? `Task '${slug}' is archived. Unarchive it before editing.` : `Task '${slug}' not found.`
|
|
6970
|
+
);
|
|
6971
|
+
return;
|
|
6972
|
+
}
|
|
6973
|
+
const { task: edited } = await updateTask(adapter, config, slug, {}, body);
|
|
6974
|
+
if (raw.json) {
|
|
6975
|
+
data(
|
|
6976
|
+
JSON.stringify(
|
|
6977
|
+
{
|
|
6978
|
+
slug: edited.slug,
|
|
6979
|
+
status: edited.frontmatter.status,
|
|
6980
|
+
frontmatter: edited.frontmatter,
|
|
6981
|
+
body: edited.body,
|
|
6982
|
+
comments: edited.comments
|
|
6983
|
+
},
|
|
6984
|
+
null,
|
|
6985
|
+
2
|
|
6986
|
+
)
|
|
6987
|
+
);
|
|
6988
|
+
return;
|
|
6989
|
+
}
|
|
6990
|
+
chrome(
|
|
6991
|
+
body.trim() ? `Updated description on ${edited.slug}` : `Cleared description on ${edited.slug}`
|
|
6992
|
+
);
|
|
6993
|
+
});
|
|
6994
|
+
const BODY_FILE_HELP = "\nPrefer --body-file for anything multi-line: the file is read as UTF-8 by\nShipBench, so the text never passes through shell quoting or encoding.\n";
|
|
6995
|
+
const commentCommand = task.command("comment").description("Manage timestamped entries in the task Updates section").argument("[slug]", "Task slug").argument("[text]", "Update text").addOption(updateTextOption()).addOption(updateTextFileOption()).addHelpText("after", BODY_FILE_HELP);
|
|
6996
|
+
commentCommand.action(async (slug, text, raw) => {
|
|
6997
|
+
const resolved = await resolveUpdateText(commentCommand, text, raw);
|
|
6998
|
+
if (!slug || resolved === void 0) {
|
|
6834
6999
|
throw new InvalidArgumentError(
|
|
6835
|
-
"Append requires a task slug and update text."
|
|
7000
|
+
"Append requires a task slug and update text (positional, --body <text>, or --body-file <path>)."
|
|
6836
7001
|
);
|
|
6837
7002
|
}
|
|
6838
7003
|
const config = await loadCliConfig();
|
|
6839
|
-
const updated = await addComment(adapter, config, slug,
|
|
7004
|
+
const updated = await addComment(adapter, config, slug, resolved);
|
|
6840
7005
|
chrome(`Added update to ${updated.slug}`);
|
|
6841
7006
|
});
|
|
6842
|
-
|
|
7007
|
+
const commentEditCommand = commentCommand.command("edit <slug> <index> [text]").description(
|
|
6843
7008
|
"Edit an Updates entry by zero-based index without changing its timestamp"
|
|
6844
|
-
).
|
|
7009
|
+
).addOption(updateTextOption()).addOption(updateTextFileOption()).addHelpText("after", BODY_FILE_HELP);
|
|
7010
|
+
commentEditCommand.action(async (slug, index, text, raw) => {
|
|
7011
|
+
const resolved = await resolveUpdateText(commentEditCommand, text, raw);
|
|
7012
|
+
if (resolved === void 0) {
|
|
7013
|
+
commentEditCommand.error(
|
|
7014
|
+
"Provide the replacement text positionally, or with --body <text> or --body-file <path>."
|
|
7015
|
+
);
|
|
7016
|
+
return;
|
|
7017
|
+
}
|
|
6845
7018
|
const config = await loadCliConfig();
|
|
6846
7019
|
const parsedIndex = parseNonNegativeInteger(index);
|
|
6847
7020
|
const updated = await editComment(
|
|
@@ -6849,11 +7022,11 @@ function createCli(opts) {
|
|
|
6849
7022
|
config,
|
|
6850
7023
|
slug,
|
|
6851
7024
|
parsedIndex,
|
|
6852
|
-
|
|
7025
|
+
resolved
|
|
6853
7026
|
);
|
|
6854
7027
|
chrome(`Edited update ${parsedIndex} on ${updated.slug}`);
|
|
6855
7028
|
});
|
|
6856
|
-
|
|
7029
|
+
commentCommand.command("delete <slug> <index>").description("Delete an Updates entry by zero-based index").action(async (slug, index) => {
|
|
6857
7030
|
const config = await loadCliConfig();
|
|
6858
7031
|
const parsedIndex = parseNonNegativeInteger(index);
|
|
6859
7032
|
const updated = await deleteComment(adapter, config, slug, parsedIndex);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipbench",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Git-native project management for solo developers. Your task board lives in your repository as Markdown.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"chokidar": "^5.0.0",
|
|
35
35
|
"commander": "^15.0.0",
|
|
36
|
-
"@shipbench/board": "0.
|
|
36
|
+
"@shipbench/board": "0.4.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^26.0.1",
|
|
40
40
|
"gray-matter": "^4.0.3",
|
|
41
41
|
"tsup": "^8.0.0",
|
|
42
42
|
"typescript": "^5.5.0",
|
|
43
|
-
"@shipbench/core": "0.
|
|
43
|
+
"@shipbench/core": "0.4.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|