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.
Files changed (2) hide show
  1. package/dist/index.js +205 -32
  2. 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(/^###\s+(.+?)\s*$/);
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[1];
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
- - **Edit an update's text**: \`shipbench task comment edit <slug> <index> "Corrected text."\`
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.2.0" : "0.0.0-dev";
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 program = new Command().name("shipbench").description("Git-native project management for solo developers.").version(VERSION, "-v, --version", "output the version").option(
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").action(async (title, raw) => {
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(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
- });
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 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
- if (!slug || text === void 0) {
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, text);
7004
+ const updated = await addComment(adapter, config, slug, resolved);
6840
7005
  chrome(`Added update to ${updated.slug}`);
6841
7006
  });
6842
- comment.command("edit <slug> <index> <text>").description(
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
- ).action(async (slug, index, text) => {
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
- text
7025
+ resolved
6853
7026
  );
6854
7027
  chrome(`Edited update ${parsedIndex} on ${updated.slug}`);
6855
7028
  });
6856
- comment.command("delete <slug> <index>").description("Delete an Updates entry by zero-based index").action(async (slug, index) => {
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.2.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.2.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.2.0"
43
+ "@shipbench/core": "0.4.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",