shipbench 0.1.1 → 0.3.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 +117 -15
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4153,6 +4153,17 @@ function parseTaskBody(rawBody) {
|
|
|
4153
4153
|
}
|
|
4154
4154
|
return { body: description, comments };
|
|
4155
4155
|
}
|
|
4156
|
+
function assertBodyWithoutUpdatesMarker(body) {
|
|
4157
|
+
let fence = null;
|
|
4158
|
+
for (const line of body.split(/\r?\n/)) {
|
|
4159
|
+
if (!fence && line.trimEnd() === UPDATES_HEADING) {
|
|
4160
|
+
throw new Error(
|
|
4161
|
+
`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.`
|
|
4162
|
+
);
|
|
4163
|
+
}
|
|
4164
|
+
fence = updateFence(line, fence);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4156
4167
|
function parseFrontmatter(fileContent) {
|
|
4157
4168
|
try {
|
|
4158
4169
|
return (0, import_gray_matter.default)(fileContent);
|
|
@@ -4360,7 +4371,8 @@ function taskFileSlugs(result) {
|
|
|
4360
4371
|
])
|
|
4361
4372
|
];
|
|
4362
4373
|
}
|
|
4363
|
-
async function createTask(adapter, config, title, fields) {
|
|
4374
|
+
async function createTask(adapter, config, title, fields, body) {
|
|
4375
|
+
if (body !== void 0) assertBodyWithoutUpdatesMarker(body);
|
|
4364
4376
|
const [existingFiles, archivedFiles] = await Promise.all([
|
|
4365
4377
|
adapter.listFiles(TASKS_DIR),
|
|
4366
4378
|
adapter.listFiles(ARCHIVE_DIR)
|
|
@@ -4394,7 +4406,7 @@ async function createTask(adapter, config, title, fields) {
|
|
|
4394
4406
|
created: now,
|
|
4395
4407
|
updated: now
|
|
4396
4408
|
},
|
|
4397
|
-
body: "",
|
|
4409
|
+
body: body ?? "",
|
|
4398
4410
|
comments: []
|
|
4399
4411
|
};
|
|
4400
4412
|
await adapter.writeFile(`${TASKS_DIR}/${slug}.md`, serializeTask(task));
|
|
@@ -4408,6 +4420,7 @@ async function createTask(adapter, config, title, fields) {
|
|
|
4408
4420
|
return task;
|
|
4409
4421
|
}
|
|
4410
4422
|
async function updateTask(adapter, config, slug, fields, body) {
|
|
4423
|
+
if (body !== void 0) assertBodyWithoutUpdatesMarker(body);
|
|
4411
4424
|
const path = `${TASKS_DIR}/${slug}.md`;
|
|
4412
4425
|
const content = await adapter.readFile(path);
|
|
4413
4426
|
const task = parseTaskFile(slug, content);
|
|
@@ -4810,7 +4823,7 @@ Visible order comes from \`config.json\`, the task files, and this partial index
|
|
|
4810
4823
|
- The Uncategorized column and the \`done_column\` both ignore \`layout\` entirely.
|
|
4811
4824
|
- The CLI and Board do not record \`layout[done_column]\`; any existing entry is removed on the next layout write.
|
|
4812
4825
|
|
|
4813
|
-
Do not read \`layout.json\` alone to determine board order. \`shipbench task list\` returns live tasks in canonical board order,
|
|
4826
|
+
Do not read \`layout.json\` alone to determine board order. \`shipbench task list --json\` returns live tasks in canonical board order, including each task's zero-based \`position\` within its column. Direct file readers must apply the rules above. Treat the index as machine-managed: do not hand-edit or hand-order it. You may gitignore it if ordering should stay machine-local, but Harbor and fresh clones will then fall back to deterministic \`created\`-descending order for unlisted tasks.
|
|
4814
4827
|
|
|
4815
4828
|
## Task files
|
|
4816
4829
|
|
|
@@ -4818,6 +4831,8 @@ Every file in \`tasks/\` is a Markdown document with a YAML frontmatter block. S
|
|
|
4818
4831
|
|
|
4819
4832
|
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
4833
|
|
|
4834
|
+
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.
|
|
4835
|
+
|
|
4821
4836
|
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
4837
|
|
|
4823
4838
|
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/\`.
|
|
@@ -4890,6 +4905,8 @@ This heuristic is guidance, not a validation rule. Core stores each entry as \`{
|
|
|
4890
4905
|
|
|
4891
4906
|
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
4907
|
|
|
4908
|
+
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. Put the heading in a code fence when a description means it literally.
|
|
4909
|
+
|
|
4893
4910
|
Do not hand-edit content below the \`## Task Updates\` marker when the CLI is available.
|
|
4894
4911
|
|
|
4895
4912
|
## Choosing What to Work On
|
|
@@ -4944,7 +4961,7 @@ Note that \`depends_on\` and the task's column are orthogonal. A column says whe
|
|
|
4944
4961
|
|
|
4945
4962
|
\`layout.json\` is a partial, machine-managed index, not the visible order. It can omit \`done_column\`, unlisted tasks, and whole columns; retain stale slugs until another layout write; or be absent or gitignored. Reading it alone can therefore give the wrong answer.
|
|
4946
4963
|
|
|
4947
|
-
\`shipbench task list\` reports live tasks in configured column order and visible within-column order
|
|
4964
|
+
\`shipbench task list --json\` reports live tasks in configured column order and visible within-column order, including each task's zero-based \`position\` within its column. When working directly with the plain files, combine task statuses with \`config.json\` and the ordering rules in \`README.md\`; do not use \`layout.json\` alone as the answer.
|
|
4948
4965
|
|
|
4949
4966
|
## Changing Board Order
|
|
4950
4967
|
|
|
@@ -4973,6 +4990,8 @@ Prefer the ShipBench CLI for task mutations when it is available. The CLI routes
|
|
|
4973
4990
|
- **Inspect dependencies**: \`shipbench task graph --json\`
|
|
4974
4991
|
- **Include descriptions in a list**: \`shipbench task list --json --include-body\`
|
|
4975
4992
|
- **Create a task**: \`shipbench task create "Task title" --status=todo\`
|
|
4993
|
+
- **Create a task with a description**: \`shipbench task create "Task title" --body-file=description.md\` (or \`--body "One-line description."\`)
|
|
4994
|
+
- **Rewrite a description**: \`shipbench task edit <slug> --body-file=description.md\` (replaces it whole; \`--body ""\` clears it)
|
|
4976
4995
|
- **Create a dependent task**: \`shipbench task create "Task title" --depends-on=other-slug,another-slug\`
|
|
4977
4996
|
- **Add a time-anchored update**: \`shipbench task comment <slug> "What changed and why."\`
|
|
4978
4997
|
- **Edit an update's text**: \`shipbench task comment edit <slug> <index> "Corrected text."\`
|
|
@@ -4994,7 +5013,7 @@ Use direct edits only when the CLI is unavailable or when changing task descript
|
|
|
4994
5013
|
|
|
4995
5014
|
- **Create a task**: Add a new \`.md\` file in \`tasks/\` following the format above.
|
|
4996
5015
|
- **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\`.
|
|
5016
|
+
- **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
5017
|
- **Add an Update without the CLI**: Append a \`### <ISO 8601 timestamp>\` heading and text below the trailing \`## Task Updates\` marker.
|
|
4999
5018
|
- **Edit an Update without the CLI**: Change only its text; preserve the \`###\` timestamp heading and update the frontmatter \`updated\` value.
|
|
5000
5019
|
- **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 +5023,7 @@ Use direct edits only when the CLI is unavailable or when changing task descript
|
|
|
5004
5023
|
|
|
5005
5024
|
- Never invent status values not listed in \`config.json\`.
|
|
5006
5025
|
- Reorder tasks only when the user explicitly asks for it.
|
|
5007
|
-
- Always update the \`updated\` timestamp when modifying a task.
|
|
5026
|
+
- Always update the \`updated\` timestamp when modifying a task by hand. Every CLI mutation maintains it for you.
|
|
5008
5027
|
- Do not modify \`config.json\` unless explicitly asked.
|
|
5009
5028
|
- Do not read \`layout.json\` as the visible order or modify it; the CLI and Board own this partial index.
|
|
5010
5029
|
- Do not read or modify \`tasks/archive/\` unless the user explicitly asks about archived work.
|
|
@@ -5357,6 +5376,7 @@ import { statSync } from "fs";
|
|
|
5357
5376
|
import { basename, resolve as resolve3 } from "path";
|
|
5358
5377
|
|
|
5359
5378
|
// src/cli.ts
|
|
5379
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5360
5380
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
5361
5381
|
|
|
5362
5382
|
// src/boardServer.ts
|
|
@@ -6567,7 +6587,7 @@ async function runTui(options2) {
|
|
|
6567
6587
|
}
|
|
6568
6588
|
|
|
6569
6589
|
// src/cli.ts
|
|
6570
|
-
var VERSION = true ? "0.
|
|
6590
|
+
var VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
6571
6591
|
function commaList(value) {
|
|
6572
6592
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6573
6593
|
}
|
|
@@ -6656,6 +6676,23 @@ function formatTaskDependencyGraph(graph) {
|
|
|
6656
6676
|
}
|
|
6657
6677
|
return lines;
|
|
6658
6678
|
}
|
|
6679
|
+
function bodyOption() {
|
|
6680
|
+
return new Option(
|
|
6681
|
+
"--body <text>",
|
|
6682
|
+
"Description as Markdown text"
|
|
6683
|
+
).conflicts(["bodyFile"]);
|
|
6684
|
+
}
|
|
6685
|
+
function bodyFileOption() {
|
|
6686
|
+
return new Option(
|
|
6687
|
+
"--body-file <path>",
|
|
6688
|
+
'Read the description from a UTF-8 file; "-" reads stdin'
|
|
6689
|
+
).conflicts(["body"]);
|
|
6690
|
+
}
|
|
6691
|
+
async function readProcessStdin() {
|
|
6692
|
+
const chunks = [];
|
|
6693
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
6694
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
6695
|
+
}
|
|
6659
6696
|
function enableExitOverride(command) {
|
|
6660
6697
|
command.exitOverride();
|
|
6661
6698
|
for (const child of command.commands) enableExitOverride(child);
|
|
@@ -6684,6 +6721,19 @@ function createCli(opts) {
|
|
|
6684
6721
|
});
|
|
6685
6722
|
const fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
|
|
6686
6723
|
const runGit = opts.runGit ?? runGitCommand;
|
|
6724
|
+
const readTextFile = opts.readTextFile ?? ((path) => readFile3(path, "utf8"));
|
|
6725
|
+
const readStdin = opts.readStdin ?? readProcessStdin;
|
|
6726
|
+
const resolveBody = async (raw) => {
|
|
6727
|
+
if (raw.bodyFile === void 0) return raw.body;
|
|
6728
|
+
if (raw.bodyFile === "-") return readStdin();
|
|
6729
|
+
try {
|
|
6730
|
+
return await readTextFile(raw.bodyFile);
|
|
6731
|
+
} catch (error) {
|
|
6732
|
+
throw new Error(
|
|
6733
|
+
`Cannot read --body-file "${raw.bodyFile}": ${error instanceof Error ? error.message : String(error)}`
|
|
6734
|
+
);
|
|
6735
|
+
}
|
|
6736
|
+
};
|
|
6687
6737
|
const program = new Command().name("shipbench").description("Git-native project management for solo developers.").version(VERSION, "-v, --version", "output the version").option(
|
|
6688
6738
|
"-C <path>",
|
|
6689
6739
|
"Run as if ShipBench was started in the specified directory"
|
|
@@ -6802,15 +6852,25 @@ function createCli(opts) {
|
|
|
6802
6852
|
"Slugs this task depends on (comma-separated, repeatable)",
|
|
6803
6853
|
accumulateCommaList,
|
|
6804
6854
|
[]
|
|
6805
|
-
).option("--json", "Output the created task as JSON").
|
|
6855
|
+
).addOption(bodyOption()).addOption(bodyFileOption()).option("--json", "Output the created task as JSON").addHelpText(
|
|
6856
|
+
"after",
|
|
6857
|
+
"\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"
|
|
6858
|
+
).action(async (title, raw) => {
|
|
6859
|
+
const body = await resolveBody(raw);
|
|
6806
6860
|
const config = await loadCliConfig();
|
|
6807
|
-
const created = await createTask(
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6861
|
+
const created = await createTask(
|
|
6862
|
+
adapter,
|
|
6863
|
+
config,
|
|
6864
|
+
title,
|
|
6865
|
+
{
|
|
6866
|
+
status: raw.status,
|
|
6867
|
+
assignee: raw.assignee,
|
|
6868
|
+
priority: raw.priority,
|
|
6869
|
+
tags: raw.tags,
|
|
6870
|
+
depends_on: raw.dependsOn
|
|
6871
|
+
},
|
|
6872
|
+
body
|
|
6873
|
+
);
|
|
6814
6874
|
if (raw.json) {
|
|
6815
6875
|
data(
|
|
6816
6876
|
JSON.stringify(
|
|
@@ -6829,6 +6889,48 @@ function createCli(opts) {
|
|
|
6829
6889
|
}
|
|
6830
6890
|
chrome(`Created task: ${created.slug}`);
|
|
6831
6891
|
});
|
|
6892
|
+
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(
|
|
6893
|
+
"after",
|
|
6894
|
+
"\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"
|
|
6895
|
+
);
|
|
6896
|
+
editCommand.action(async (slug, raw) => {
|
|
6897
|
+
const body = await resolveBody(raw);
|
|
6898
|
+
if (body === void 0) {
|
|
6899
|
+
editCommand.error(
|
|
6900
|
+
'Provide --body <text> or --body-file <path> (use "-" to read stdin).'
|
|
6901
|
+
);
|
|
6902
|
+
return;
|
|
6903
|
+
}
|
|
6904
|
+
const config = await loadCliConfig();
|
|
6905
|
+
const existing = await getTask(adapter, config, slug);
|
|
6906
|
+
if (!existing) {
|
|
6907
|
+
const archived = await getTask(adapter, config, slug, { archived: true });
|
|
6908
|
+
editCommand.error(
|
|
6909
|
+
archived ? `Task '${slug}' is archived. Unarchive it before editing.` : `Task '${slug}' not found.`
|
|
6910
|
+
);
|
|
6911
|
+
return;
|
|
6912
|
+
}
|
|
6913
|
+
const { task: edited } = await updateTask(adapter, config, slug, {}, body);
|
|
6914
|
+
if (raw.json) {
|
|
6915
|
+
data(
|
|
6916
|
+
JSON.stringify(
|
|
6917
|
+
{
|
|
6918
|
+
slug: edited.slug,
|
|
6919
|
+
status: edited.frontmatter.status,
|
|
6920
|
+
frontmatter: edited.frontmatter,
|
|
6921
|
+
body: edited.body,
|
|
6922
|
+
comments: edited.comments
|
|
6923
|
+
},
|
|
6924
|
+
null,
|
|
6925
|
+
2
|
|
6926
|
+
)
|
|
6927
|
+
);
|
|
6928
|
+
return;
|
|
6929
|
+
}
|
|
6930
|
+
chrome(
|
|
6931
|
+
body.trim() ? `Updated description on ${edited.slug}` : `Cleared description on ${edited.slug}`
|
|
6932
|
+
);
|
|
6933
|
+
});
|
|
6832
6934
|
const comment = task.command("comment").description("Manage timestamped entries in the task Updates section").argument("[slug]", "Task slug").argument("[text]", "Update text").action(async (slug, text) => {
|
|
6833
6935
|
if (!slug || text === void 0) {
|
|
6834
6936
|
throw new InvalidArgumentError(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipbench",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.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.3.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsup",
|