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.
Files changed (2) hide show
  1. package/dist/index.js +117 -15
  2. 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, and its JSON output includes each task's zero-based \`position\` within its column. Code clients can apply \`orderedTasksForColumn\` to the task files. 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.
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; JSON output includes each task's zero-based \`position\` within its column. Code clients can apply \`orderedTasksForColumn\` to the task files. 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.
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.1.1" : "0.0.0-dev";
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").action(async (title, raw) => {
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(adapter, config, title, {
6808
- status: raw.status,
6809
- assignee: raw.assignee,
6810
- priority: raw.priority,
6811
- tags: raw.tags,
6812
- depends_on: raw.dependsOn
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.1.1",
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.1.1"
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.1.1"
43
+ "@shipbench/core": "0.3.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",