schub 0.1.2 → 0.1.3

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 CHANGED
@@ -35183,7 +35183,7 @@ var import_react28 = __toESM(require_react(), 1);
35183
35183
  // package.json
35184
35184
  var package_default = {
35185
35185
  name: "schub",
35186
- version: "0.1.2",
35186
+ version: "0.1.3",
35187
35187
  type: "module",
35188
35188
  bin: {
35189
35189
  schub: "./src/index.ts"
@@ -35265,11 +35265,11 @@ var createTask = (schubDir, options) => {
35265
35265
  const changeId = options.changeId.trim();
35266
35266
  const status = (options.status || "backlog").trim();
35267
35267
  const titles = options.titles.map((t) => t.trim()).filter(Boolean);
35268
- if (!/^(?:[Cc]\d{3}_)?[a-z0-9]+(?:-[a-z0-9]+)*$/.test(changeId)) {
35269
- throw new Error(`Invalid change-id '${changeId}'. Use kebab-case or a C-prefixed id (e.g., C001_add-user-auth).`);
35268
+ if (!/^(?:[Cc]\d{4}_)?[a-z0-9]+(?:-[a-z0-9]+)*$/.test(changeId)) {
35269
+ throw new Error(`Invalid change-id '${changeId}'. Use kebab-case or a C-prefixed id (e.g., C0001_add-user-auth).`);
35270
35270
  }
35271
35271
  let normalizedChangeId = changeId;
35272
- const match = changeId.match(/^([Cc])(\d{3})_(.+)$/);
35272
+ const match = changeId.match(/^([Cc])(\d{4})_(.+)$/);
35273
35273
  if (match) {
35274
35274
  normalizedChangeId = `C${match[2]}_${match[3]}`;
35275
35275
  }
@@ -35308,7 +35308,7 @@ Mark the proposal as Accepted before scaffolding tasks.`);
35308
35308
  if (entry.isDirectory()) {
35309
35309
  scan(join2(dir, entry.name));
35310
35310
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
35311
- const m = entry.name.match(/(?:^|-)T(\d{3})(?:_[^.]+)?\.md$/);
35311
+ const m = entry.name.match(/(?:^|-)T(\d{4})(?:_[^.]+)?\.md$/);
35312
35312
  if (m)
35313
35313
  existingNumbers.add(Number.parseInt(m[1], 10));
35314
35314
  }
@@ -35322,7 +35322,7 @@ Mark the proposal as Accepted before scaffolding tasks.`);
35322
35322
  const template = readTaskTemplate(schubDir);
35323
35323
  const createdPaths = [];
35324
35324
  for (const title of titles) {
35325
- const taskId = `T${nextNumber.toString().padStart(3, "0")}`;
35325
+ const taskId = `T${nextNumber.toString().padStart(4, "0")}`;
35326
35326
  let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
35327
35327
  if (!slug)
35328
35328
  slug = "task";
@@ -35339,8 +35339,8 @@ Mark the proposal as Accepted before scaffolding tasks.`);
35339
35339
  return createdPaths;
35340
35340
  };
35341
35341
  // src/features/tasks/filesystem.ts
35342
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
35343
- import { dirname, join as join3, relative, resolve } from "node:path";
35342
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, statSync as statSync2 } from "node:fs";
35343
+ import { basename, dirname, join as join3, relative, resolve } from "node:path";
35344
35344
 
35345
35345
  // src/features/tasks/sorting.ts
35346
35346
  var taskNumber = (id) => {
@@ -35388,6 +35388,9 @@ var parseTaskFilename = (fileName) => {
35388
35388
  return null;
35389
35389
  }
35390
35390
  const id = baseName.slice(0, underscoreIndex);
35391
+ if (!/^T\d{4}$/.test(id)) {
35392
+ return null;
35393
+ }
35391
35394
  const titleSlug = baseName.slice(underscoreIndex + 1);
35392
35395
  return { id, title: titleSlug.replace(/-/g, " ") };
35393
35396
  };
@@ -35441,7 +35444,7 @@ var parseTaskFile = (filePath, fallback) => {
35441
35444
  if (dependsMatch) {
35442
35445
  const raw = dependsMatch[1].trim();
35443
35446
  if (!/^none$/i.test(raw)) {
35444
- const matches = raw.match(/\bT\d+\b/g);
35447
+ const matches = raw.match(/\bT\d{4}\b/g);
35445
35448
  if (matches) {
35446
35449
  dependsOn = Array.from(new Set(matches));
35447
35450
  }
@@ -35537,6 +35540,25 @@ var loadTaskDependencies = (schubDir, statuses = TASK_STATUSES) => {
35537
35540
  }
35538
35541
  return tasks.sort(compareTasks);
35539
35542
  };
35543
+ var ACTIVE_TASK_STATUSES = TASK_STATUSES.filter((status) => status !== "archived");
35544
+ var archiveTasksForChange = (schubDir, changeId) => {
35545
+ const normalizedChangeId = changeId.trim();
35546
+ const tasksRoot = join3(schubDir, "tasks");
35547
+ const archiveRoot = join3(tasksRoot, "archived");
35548
+ mkdirSync2(archiveRoot, { recursive: true });
35549
+ const repoRoot = dirname(schubDir);
35550
+ const tasks = loadTaskDependencies(schubDir, ACTIVE_TASK_STATUSES).filter((task) => task.changeId === normalizedChangeId);
35551
+ return tasks.map((task) => {
35552
+ const currentPath = join3(repoRoot, task.path);
35553
+ const archivePath = join3(archiveRoot, basename(task.path));
35554
+ renameSync(currentPath, archivePath);
35555
+ return {
35556
+ ...task,
35557
+ status: "archived",
35558
+ path: relative(repoRoot, archivePath)
35559
+ };
35560
+ });
35561
+ };
35540
35562
  // src/features/tasks/graph.ts
35541
35563
  var buildTaskGraph = (tasks) => {
35542
35564
  const tasksById = new Map(tasks.map((task) => [task.id, task]));
@@ -35774,13 +35796,13 @@ import { dirname as dirname5, normalize, sep } from "node:path";
35774
35796
  var import_react27 = __toESM(require_react(), 1);
35775
35797
 
35776
35798
  // src/changes.ts
35777
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
35799
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
35778
35800
  import { dirname as dirname4, join as join5, relative as relative2 } from "node:path";
35779
35801
  import { fileURLToPath as fileURLToPath2 } from "node:url";
35780
35802
 
35781
35803
  // src/schub-root.ts
35782
35804
  import { statSync as statSync3 } from "node:fs";
35783
- import { basename, dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
35805
+ import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
35784
35806
  var isDirectory2 = (path) => {
35785
35807
  try {
35786
35808
  return statSync3(path).isDirectory();
@@ -35793,7 +35815,7 @@ var resolveSchubRoot = (startDir = process.cwd()) => {
35793
35815
  const fallback = join4(start, ".schub");
35794
35816
  let current = start;
35795
35817
  while (true) {
35796
- if (basename(current) === ".schub" && isDirectory2(current)) {
35818
+ if (basename2(current) === ".schub" && isDirectory2(current)) {
35797
35819
  return current;
35798
35820
  }
35799
35821
  const candidate = join4(current, ".schub");
@@ -35809,7 +35831,7 @@ var resolveSchubRoot = (startDir = process.cwd()) => {
35809
35831
  };
35810
35832
 
35811
35833
  // src/changes.ts
35812
- var CHANGE_ID_PATTERN = /^(?:[Cc]\d{3}_)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
35834
+ var CHANGE_ID_PATTERN = /^(?:[Cc]\d{4}_)?[a-z0-9]+(?:-[a-z0-9]+)*$/;
35813
35835
  var isDirectory3 = (path) => {
35814
35836
  try {
35815
35837
  return statSync4(path).isDirectory();
@@ -35827,7 +35849,7 @@ var parseProposal = (content, changeId) => {
35827
35849
  };
35828
35850
  var normalizeChangeId = (value) => {
35829
35851
  const trimmed = value.trim();
35830
- const match = trimmed.match(/^([Cc])(\d{3})_(.+)$/);
35852
+ const match = trimmed.match(/^([Cc])(\d{4})_(.+)$/);
35831
35853
  if (match) {
35832
35854
  return `C${match[2]}_${match[3]}`;
35833
35855
  }
@@ -35840,7 +35862,7 @@ var readChangeSummary = (schubDir, changeId) => {
35840
35862
  throw new Error("Provide --change-id.");
35841
35863
  }
35842
35864
  if (!isValidChangeId(trimmed)) {
35843
- throw new Error(`Invalid change-id '${changeId}'. Use kebab-case or a C-prefixed id (e.g., C001_add-user-auth).`);
35865
+ throw new Error(`Invalid change-id '${changeId}'. Use kebab-case or a C-prefixed id (e.g., C0001_add-user-auth).`);
35844
35866
  }
35845
35867
  const normalized = normalizeChangeId(trimmed);
35846
35868
  const changeDir = join5(schubDir, "changes", normalized);
@@ -35881,6 +35903,24 @@ Add a '**Status**: <value>' line before updating status.`);
35881
35903
  status: nextStatus
35882
35904
  };
35883
35905
  };
35906
+ var archiveChange = (schubDir, changeId) => {
35907
+ const summary = readChangeSummary(schubDir, changeId);
35908
+ const archiveRoot = join5(schubDir, "archive", "changes");
35909
+ const archivePath = join5(archiveRoot, summary.changeId);
35910
+ if (existsSync4(archivePath)) {
35911
+ throw new Error(`Archive already exists: ${archivePath}`);
35912
+ }
35913
+ mkdirSync3(archiveRoot, { recursive: true });
35914
+ const updated = updateChangeStatus(schubDir, summary.changeId, "Archived");
35915
+ renameSync2(summary.changeDir, archivePath);
35916
+ return {
35917
+ changeId: updated.changeId,
35918
+ previousStatus: updated.previousStatus,
35919
+ status: updated.status,
35920
+ proposalPath: join5(archivePath, "proposal.md"),
35921
+ archivePath
35922
+ };
35923
+ };
35884
35924
  var changeNumber = (id) => {
35885
35925
  const match = id.match(/\d+/);
35886
35926
  return match ? Number(match[0]) : Number.POSITIVE_INFINITY;
@@ -35925,7 +35965,7 @@ var slugify = (value) => {
35925
35965
  return slug.replace(/^-|-$/g, "");
35926
35966
  };
35927
35967
  var splitPrefixedChangeId = (changeId) => {
35928
- const match = changeId.match(/^([Cc])(\d{3})_([a-z0-9]+(?:-[a-z0-9]+)*)$/);
35968
+ const match = changeId.match(/^([Cc])(\d{4})_([a-z0-9]+(?:-[a-z0-9]+)*)$/);
35929
35969
  if (match) {
35930
35970
  return { prefix: match[2], slug: match[3] };
35931
35971
  }
@@ -35942,7 +35982,7 @@ var nextChangePrefix = (schubDir) => {
35942
35982
  if (!entry.isDirectory()) {
35943
35983
  continue;
35944
35984
  }
35945
- const match = entry.name.match(/^[Cc](\d{3})_/);
35985
+ const match = entry.name.match(/^[Cc](\d{4})_/);
35946
35986
  if (match) {
35947
35987
  prefixes.push(Number.parseInt(match[1], 10));
35948
35988
  }
@@ -35951,7 +35991,7 @@ var nextChangePrefix = (schubDir) => {
35951
35991
  scan(changesRoot);
35952
35992
  scan(archiveRoot);
35953
35993
  const next = prefixes.length > 0 ? Math.max(...prefixes) + 1 : 1;
35954
- return next.toString().padStart(3, "0");
35994
+ return next.toString().padStart(4, "0");
35955
35995
  };
35956
35996
  var CHANGE_PREFIX = "C";
35957
35997
  var BUNDLED_PROPOSAL_TEMPLATE_PATH = fileURLToPath2(new URL("../templates/create-proposal/proposal-template.md", import.meta.url));
@@ -36024,17 +36064,25 @@ var createChange = (schubDir, options) => {
36024
36064
  if (existsSync4(proposalPath) && !options.overwrite) {
36025
36065
  throw new Error(`Refusing to overwrite existing file: ${proposalPath}`);
36026
36066
  }
36027
- mkdirSync2(changeDir, { recursive: true });
36067
+ mkdirSync3(changeDir, { recursive: true });
36028
36068
  writeFileSync2(proposalPath, rendered, "utf8");
36029
36069
  return proposalPath;
36030
36070
  };
36031
36071
 
36072
+ // src/opencode.ts
36073
+ import { spawn as spawn2 } from "node:child_process";
36074
+ var launchOpencodeReview = (changeId) => {
36075
+ const prompt = `review ${changeId}`;
36076
+ spawn2("opencode", [prompt], { stdio: "ignore", detached: true }).unref();
36077
+ };
36078
+
36032
36079
  // src/components/StatusView.tsx
36033
36080
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
36034
36081
  var DEFAULT_REFRESH_INTERVAL_MS2 = 1000;
36035
- var ACTIVE_TASK_STATUSES = ["blocked", "wip", "ready", "backlog"];
36036
- var CHANGE_TASK_STATUSES = [...ACTIVE_TASK_STATUSES, "done"];
36082
+ var ACTIVE_TASK_STATUSES2 = ["blocked", "wip", "ready", "backlog"];
36083
+ var CHANGE_TASK_STATUSES = [...ACTIVE_TASK_STATUSES2, "done"];
36037
36084
  var AUTO_MARK_STATUSES = new Set(["accepted", "wip"]);
36085
+ var REVIEW_SHORTCUT = { keyLabel: "r", label: "review" };
36038
36086
  var compareText = (left, right) => left.localeCompare(right, undefined, { sensitivity: "base" });
36039
36087
  var sortByStatusThenTitle = (left, right) => {
36040
36088
  const statusCompare = compareText(left.status, right.status);
@@ -36052,9 +36100,20 @@ var isTaskItem = (item) => {
36052
36100
  return parts.includes("tasks");
36053
36101
  };
36054
36102
  var formatChangeId = (value) => {
36055
- const match = value.match(/^([Cc]\d{3})_/);
36103
+ const match = value.match(/^([Cc]\d{4})_/);
36056
36104
  return match ? match[1].toUpperCase() : value;
36057
36105
  };
36106
+ var formatChangeTitle = (changeId, title) => {
36107
+ const trimmedTitle = title.trim();
36108
+ if (trimmedTitle !== changeId) {
36109
+ return trimmedTitle;
36110
+ }
36111
+ const match = changeId.match(/^([Cc]\d{4})_(.+)$/);
36112
+ if (!match) {
36113
+ return trimmedTitle;
36114
+ }
36115
+ return match[2].split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
36116
+ };
36058
36117
  var canAutoMarkChange = (status) => AUTO_MARK_STATUSES.has(status.trim().toLowerCase());
36059
36118
  var autoMarkChangesDone = (schubDir) => {
36060
36119
  const allTasks = loadTaskDependencies(schubDir, CHANGE_TASK_STATUSES);
@@ -36167,7 +36226,12 @@ var buildStatusData = (schubDir) => {
36167
36226
  pendingImplementationCounts
36168
36227
  };
36169
36228
  };
36170
- function StatusView({ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS2, onCopyId }) {
36229
+ function StatusView({
36230
+ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS2,
36231
+ onCopyId,
36232
+ onReview = launchOpencodeReview,
36233
+ onShortcutsChange
36234
+ }) {
36171
36235
  const schubDir = findSchubRoot();
36172
36236
  const [, setRefreshTick] = import_react27.default.useState(0);
36173
36237
  const {
@@ -36194,6 +36258,20 @@ function StatusView({ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS2, onCopyId
36194
36258
  ];
36195
36259
  const [selection, setSelection] = import_react27.default.useState(0);
36196
36260
  const totalItems = allItems.length;
36261
+ const pendingReviewIds = new Set(pendingReview.map((change) => change.id));
36262
+ const selectedItem = totalItems > 0 ? allItems[selection] : null;
36263
+ const selectedReviewId = selectedItem && !isTaskItem(selectedItem) && pendingReviewIds.has(selectedItem.id) ? selectedItem.id : null;
36264
+ const lastReviewIdRef = import_react27.default.useRef(null);
36265
+ import_react27.default.useEffect(() => {
36266
+ if (!onShortcutsChange) {
36267
+ return;
36268
+ }
36269
+ if (lastReviewIdRef.current === selectedReviewId) {
36270
+ return;
36271
+ }
36272
+ lastReviewIdRef.current = selectedReviewId;
36273
+ onShortcutsChange(selectedReviewId ? [REVIEW_SHORTCUT] : []);
36274
+ }, [onShortcutsChange, selectedReviewId]);
36197
36275
  import_react27.default.useEffect(() => {
36198
36276
  if (!schubDir) {
36199
36277
  return;
@@ -36224,12 +36302,15 @@ function StatusView({ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS2, onCopyId
36224
36302
  setSelection((current) => Math.max(current - 1, 0));
36225
36303
  }
36226
36304
  if (input === "o") {
36227
- const selectedItem = allItems[selection];
36228
- openInVsCode(repoRoot, selectedItem.path);
36305
+ const selectedItem2 = allItems[selection];
36306
+ openInVsCode(repoRoot, selectedItem2.path);
36229
36307
  }
36230
36308
  if (input === "c") {
36231
- const selectedItem = allItems[selection];
36232
- onCopyId(selectedItem.id);
36309
+ const selectedItem2 = allItems[selection];
36310
+ onCopyId(selectedItem2.id);
36311
+ }
36312
+ if (input === "r" && selectedReviewId) {
36313
+ onReview(selectedReviewId);
36233
36314
  }
36234
36315
  });
36235
36316
  if (!schubDir) {
@@ -36261,7 +36342,7 @@ function StatusView({ refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS2, onCopyId
36261
36342
  const task = isTaskItem(item) ? item : null;
36262
36343
  const title = task ? trimTaskTitle(task.title) : "";
36263
36344
  const checklistIndicator = task && task.status === "wip" && typeof task.checklistTotal === "number" ? ` (${task.checklistRemaining ?? 0}/${task.checklistTotal})` : "";
36264
- const changeTitle = item.title;
36345
+ const changeTitle = task ? "" : formatChangeTitle(item.id, item.title);
36265
36346
  const changeDetail = detail ? ` ${detail}` : "";
36266
36347
  const displayTitle = task ? `${title}${checklistIndicator}` : `${changeTitle}${changeDetail}`.trim();
36267
36348
  const displayId = task ? item.id : formatChangeId(item.id);
@@ -36386,6 +36467,7 @@ function App2({ copyToClipboard: copyToClipboard2 = copyToClipboard }) {
36386
36467
  rows: stdout.rows
36387
36468
  }));
36388
36469
  const [copyBanner, setCopyBanner] = import_react28.default.useState(null);
36470
+ const [statusShortcuts, setStatusShortcuts] = import_react28.default.useState([]);
36389
36471
  const versionLabel = `${package_default.version}`;
36390
36472
  const homeDir = homedir();
36391
36473
  const startDir = process.env.SCHUB_CWD ?? process.cwd();
@@ -36425,14 +36507,20 @@ function App2({ copyToClipboard: copyToClipboard2 = copyToClipboard }) {
36425
36507
  setMode((current) => current === "status" ? "plan" : "status");
36426
36508
  }
36427
36509
  });
36510
+ import_react28.default.useEffect(() => {
36511
+ if (mode !== "status") {
36512
+ setStatusShortcuts([]);
36513
+ }
36514
+ }, [mode]);
36428
36515
  const handleCopyId = (value) => {
36429
36516
  copyToClipboard2(value);
36430
36517
  setCopyBanner(COPY_BANNER_TEXT);
36431
36518
  };
36432
- const shortcuts = [
36519
+ const baseShortcuts = [
36433
36520
  { keyLabel: "o", label: "open file" },
36434
36521
  { keyLabel: "c", label: "copy" }
36435
36522
  ];
36523
+ const shortcuts = mode === "status" ? [...baseShortcuts, ...statusShortcuts] : baseShortcuts;
36436
36524
  return /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(Box_default, {
36437
36525
  backgroundColor: "black",
36438
36526
  flexDirection: "column",
@@ -36490,7 +36578,8 @@ function App2({ copyToClipboard: copyToClipboard2 = copyToClipboard }) {
36490
36578
  flexGrow: 1,
36491
36579
  flexShrink: 1,
36492
36580
  children: mode === "status" ? /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(StatusView, {
36493
- onCopyId: handleCopyId
36581
+ onCopyId: handleCopyId,
36582
+ onShortcutsChange: setStatusShortcuts
36494
36583
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime3.jsxDEV(PlanView, {
36495
36584
  onCopyId: handleCopyId
36496
36585
  }, undefined, false, undefined, this)
@@ -36563,7 +36652,7 @@ function App2({ copyToClipboard: copyToClipboard2 = copyToClipboard }) {
36563
36652
  }
36564
36653
 
36565
36654
  // src/commands/adr.ts
36566
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
36655
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
36567
36656
  import { dirname as dirname6, join as join6 } from "node:path";
36568
36657
  import { fileURLToPath as fileURLToPath3 } from "node:url";
36569
36658
  var BUNDLED_ADR_TEMPLATE_PATH = fileURLToPath3(new URL("../../templates/create-proposal/adr-template.md", import.meta.url));
@@ -36640,12 +36729,11 @@ var runAdrCreate = (args, startDir) => {
36640
36729
  const templatePath = resolveTemplatePath(schubDir, join6("create-proposal", "adr-template.md"), BUNDLED_ADR_TEMPLATE_PATH);
36641
36730
  const template = readFileSync5(templatePath, "utf8");
36642
36731
  const rendered = renderAdrTemplate(template, adrTitle || summary.changeId);
36643
- mkdirSync3(dirname6(outputPath), { recursive: true });
36732
+ mkdirSync4(dirname6(outputPath), { recursive: true });
36644
36733
  writeFileSync3(outputPath, rendered, "utf8");
36645
36734
  process.stdout.write(`[OK] Wrote ADR: ${outputPath}
36646
36735
  `);
36647
36736
  };
36648
-
36649
36737
  // src/commands/changes.ts
36650
36738
  var parseChangeCreateOptions = (args) => {
36651
36739
  let changeId;
@@ -36771,6 +36859,72 @@ var parseChangeStatusOptions = (args) => {
36771
36859
  const options = { changeId, status };
36772
36860
  return options;
36773
36861
  };
36862
+ var parseChangeArchiveOptions = (args) => {
36863
+ let changeId;
36864
+ let skipTasks = false;
36865
+ const unknown = [];
36866
+ const rejectUnsupported = (flag) => {
36867
+ throw new Error(`Unsupported option: ${flag}.`);
36868
+ };
36869
+ for (let index = 0;index < args.length; index += 1) {
36870
+ const arg = args[index];
36871
+ if (arg === "--skip-tasks") {
36872
+ skipTasks = true;
36873
+ continue;
36874
+ }
36875
+ if (arg === "--change-id") {
36876
+ changeId = args[index + 1];
36877
+ if (changeId === undefined) {
36878
+ throw new Error("Missing value for --change-id.");
36879
+ }
36880
+ index += 1;
36881
+ continue;
36882
+ }
36883
+ if (arg.startsWith("--change-id=")) {
36884
+ changeId = arg.slice("--change-id=".length);
36885
+ continue;
36886
+ }
36887
+ if (arg === "--schub-root" || arg === "--agent-root") {
36888
+ rejectUnsupported(arg);
36889
+ }
36890
+ if (arg.startsWith("--schub-root=")) {
36891
+ rejectUnsupported("--schub-root");
36892
+ }
36893
+ if (arg.startsWith("--agent-root=")) {
36894
+ rejectUnsupported("--agent-root");
36895
+ }
36896
+ unknown.push(arg);
36897
+ }
36898
+ if (unknown.length > 0) {
36899
+ throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
36900
+ }
36901
+ if (!changeId) {
36902
+ throw new Error("Provide --change-id.");
36903
+ }
36904
+ const options = { changeId, skipTasks };
36905
+ return options;
36906
+ };
36907
+ var parseChangeListOptions = (args) => {
36908
+ const unknown = [];
36909
+ const rejectUnsupported = (flag) => {
36910
+ throw new Error(`Unsupported option: ${flag}.`);
36911
+ };
36912
+ for (const arg of args) {
36913
+ if (arg === "--schub-root" || arg === "--agent-root") {
36914
+ rejectUnsupported(arg);
36915
+ }
36916
+ if (arg.startsWith("--schub-root=")) {
36917
+ rejectUnsupported("--schub-root");
36918
+ }
36919
+ if (arg.startsWith("--agent-root=")) {
36920
+ rejectUnsupported("--agent-root");
36921
+ }
36922
+ unknown.push(arg);
36923
+ }
36924
+ if (unknown.length > 0) {
36925
+ throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
36926
+ }
36927
+ };
36774
36928
  var runChangesCreate = (args, startDir) => {
36775
36929
  const options = parseChangeCreateOptions(args);
36776
36930
  const schubDir = resolveChangeRoot(startDir);
@@ -36785,9 +36939,27 @@ var runChangesStatus = (args, startDir) => {
36785
36939
  process.stdout.write(`[OK] Updated status for ${updated.changeId}: ${updated.previousStatus} -> ${updated.status}
36786
36940
  `);
36787
36941
  };
36942
+ var runChangesArchive = (args, startDir) => {
36943
+ const options = parseChangeArchiveOptions(args);
36944
+ const schubDir = resolveChangeRoot(startDir);
36945
+ const archived = archiveChange(schubDir, options.changeId);
36946
+ const archivedTasks = options.skipTasks ? [] : archiveTasksForChange(schubDir, archived.changeId);
36947
+ const taskLabel = options.skipTasks ? "tasks kept" : `${archivedTasks.length} task${archivedTasks.length === 1 ? "" : "s"} archived`;
36948
+ process.stdout.write(`[OK] Archived change ${archived.changeId} (${taskLabel})
36949
+ `);
36950
+ };
36951
+ var runChangesList = (args, startDir) => {
36952
+ parseChangeListOptions(args);
36953
+ const schubDir = resolveChangeRoot(startDir);
36954
+ const changes = listChanges(schubDir);
36955
+ const lines = changes.map((change) => `${change.id} ${change.title} (${change.status})`);
36956
+ process.stdout.write(`${lines.join(`
36957
+ `)}
36958
+ `);
36959
+ };
36788
36960
 
36789
36961
  // src/commands/cookbook.ts
36790
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
36962
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
36791
36963
  import { dirname as dirname7, join as join7 } from "node:path";
36792
36964
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36793
36965
  var BUNDLED_COOKBOOK_TEMPLATE_PATH = fileURLToPath4(new URL("../../templates/create-proposal/cookbook-template.md", import.meta.url));
@@ -36850,14 +37022,14 @@ var runCookbookCreate = (args, startDir) => {
36850
37022
  const templatePath = resolveTemplatePath(schubDir, join7("create-proposal", "cookbook-template.md"), BUNDLED_COOKBOOK_TEMPLATE_PATH);
36851
37023
  const template = readFileSync6(templatePath, "utf8");
36852
37024
  const rendered = renderCookbookTemplate(template, summary.changeTitle, summary.changeId);
36853
- mkdirSync4(dirname7(outputPath), { recursive: true });
37025
+ mkdirSync5(dirname7(outputPath), { recursive: true });
36854
37026
  writeFileSync4(outputPath, rendered, "utf8");
36855
37027
  process.stdout.write(`[OK] Wrote cookbook: ${outputPath}
36856
37028
  `);
36857
37029
  };
36858
37030
 
36859
37031
  // src/commands/eject.ts
36860
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync5, rmSync, statSync as statSync5 } from "node:fs";
37032
+ import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync6, rmSync, statSync as statSync5 } from "node:fs";
36861
37033
  import { join as join8 } from "node:path";
36862
37034
  import { fileURLToPath as fileURLToPath5 } from "node:url";
36863
37035
  var BUNDLED_SKILLS_ROOT = fileURLToPath5(new URL("../../skills", import.meta.url));
@@ -36929,7 +37101,7 @@ Re-run with --force.`);
36929
37101
  var runEject = (args, startDir) => {
36930
37102
  const options = parseEjectOptions(args);
36931
37103
  const schubRoot = resolveSchubRoot(startDir);
36932
- mkdirSync5(schubRoot, { recursive: true });
37104
+ mkdirSync6(schubRoot, { recursive: true });
36933
37105
  const skillsTarget = join8(schubRoot, "skills");
36934
37106
  const templatesTarget = join8(schubRoot, "templates");
36935
37107
  enforceOverwrite([skillsTarget, templatesTarget], options.force);
@@ -36942,7 +37114,7 @@ var runEject = (args, startDir) => {
36942
37114
  };
36943
37115
 
36944
37116
  // src/commands/init.ts
36945
- import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
37117
+ import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
36946
37118
  import { homedir as homedir2 } from "node:os";
36947
37119
  import { join as join10, resolve as resolve5 } from "node:path";
36948
37120
  import { createInterface } from "node:readline";
@@ -36950,7 +37122,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
36950
37122
 
36951
37123
  // src/init.ts
36952
37124
  import { spawnSync as spawnSync2 } from "node:child_process";
36953
- import { mkdirSync as mkdirSync6 } from "node:fs";
37125
+ import { mkdirSync as mkdirSync7 } from "node:fs";
36954
37126
  import { join as join9, resolve as resolve4 } from "node:path";
36955
37127
  var resolveGitRoot = (startDir) => {
36956
37128
  const result = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
@@ -36969,12 +37141,15 @@ var initSchubRoot = (startDir = process.cwd()) => {
36969
37141
  const gitRoot = resolveGitRoot(resolvedStart);
36970
37142
  const root = gitRoot ?? resolvedStart;
36971
37143
  const schubRoot = join9(root, ".schub");
36972
- mkdirSync6(schubRoot, { recursive: true });
37144
+ mkdirSync7(schubRoot, { recursive: true });
36973
37145
  return schubRoot;
36974
37146
  };
36975
37147
 
36976
37148
  // src/commands/init.ts
36977
- var PROVIDERS = [{ id: "codex", label: "Codex" }];
37149
+ var PROVIDERS = [
37150
+ { id: "codex", label: "Codex" },
37151
+ { id: "opencode", label: "Opencode" }
37152
+ ];
36978
37153
  var BUNDLED_SKILLS_ROOT2 = fileURLToPath6(new URL("../../skills", import.meta.url));
36979
37154
  var isDirectory5 = (path) => {
36980
37155
  try {
@@ -37028,8 +37203,21 @@ var resolveCodexSkillsRoot = (startDir) => {
37028
37203
  const home = process.env.HOME ?? homedir2();
37029
37204
  return join10(home, ".codex", "skills");
37030
37205
  };
37031
- var installCodexSkills = (destination) => {
37032
- mkdirSync7(destination, { recursive: true });
37206
+ var resolveOpencodeSkillsRoot = (startDir) => {
37207
+ const resolvedStart = resolve5(startDir);
37208
+ const gitRoot = resolveGitRoot(resolvedStart);
37209
+ if (!gitRoot) {
37210
+ return join10(resolvedStart, ".opencode", "skills");
37211
+ }
37212
+ const localOpencodeRoot = join10(gitRoot, ".opencode");
37213
+ if (isDirectory5(localOpencodeRoot)) {
37214
+ return join10(localOpencodeRoot, "skills");
37215
+ }
37216
+ const home = process.env.HOME ?? homedir2();
37217
+ return join10(home, ".opencode", "skills");
37218
+ };
37219
+ var installBundledSkills = (destination) => {
37220
+ mkdirSync8(destination, { recursive: true });
37033
37221
  const entries = readdirSync4(BUNDLED_SKILLS_ROOT2, { withFileTypes: true });
37034
37222
  const installed = [];
37035
37223
  const skipped = [];
@@ -37048,8 +37236,10 @@ var installCodexSkills = (destination) => {
37048
37236
  }
37049
37237
  return { installed, skipped };
37050
37238
  };
37051
- var reportCodexInstall = (destination, installed, skipped) => {
37052
- process.stdout.write(`[OK] Codex skills: ${destination}
37239
+ var installCodexSkills = (destination) => installBundledSkills(destination);
37240
+ var installOpencodeSkills = (destination) => installBundledSkills(destination);
37241
+ var reportSkillsInstall = (label, destination, installed, skipped) => {
37242
+ process.stdout.write(`[OK] ${label} skills: ${destination}
37053
37243
  `);
37054
37244
  for (const skill of installed) {
37055
37245
  process.stdout.write(`[OK] Installed ${skill}
@@ -37060,6 +37250,12 @@ var reportCodexInstall = (destination, installed, skipped) => {
37060
37250
  `);
37061
37251
  }
37062
37252
  };
37253
+ var reportCodexInstall = (destination, installed, skipped) => {
37254
+ reportSkillsInstall("Codex", destination, installed, skipped);
37255
+ };
37256
+ var reportOpencodeInstall = (destination, installed, skipped) => {
37257
+ reportSkillsInstall("Opencode", destination, installed, skipped);
37258
+ };
37063
37259
  var parseInitOptions = (args) => {
37064
37260
  if (args.length === 0) {
37065
37261
  return;
@@ -37077,11 +37273,16 @@ var runInit = async (args, startDir) => {
37077
37273
  const { installed, skipped } = installCodexSkills(destination);
37078
37274
  reportCodexInstall(destination, installed, skipped);
37079
37275
  }
37276
+ if (providers.includes("opencode")) {
37277
+ const destination = resolveOpencodeSkillsRoot(startDir);
37278
+ const { installed, skipped } = installOpencodeSkills(destination);
37279
+ reportOpencodeInstall(destination, installed, skipped);
37280
+ }
37080
37281
  };
37081
37282
 
37082
37283
  // src/project.ts
37083
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
37084
- import { basename as basename2, dirname as dirname8, join as join11, resolve as resolve6 } from "node:path";
37284
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
37285
+ import { basename as basename3, dirname as dirname8, join as join11, resolve as resolve6 } from "node:path";
37085
37286
  import { fileURLToPath as fileURLToPath7 } from "node:url";
37086
37287
  var TEMPLATE_FILES = {
37087
37288
  "project-overview.md": "project-overview-template.md",
@@ -37100,11 +37301,11 @@ var writeOutput = (path, content, overwrite) => {
37100
37301
  if (existsSync9(path) && !overwrite) {
37101
37302
  throw new Error(`[ERROR] Refusing to overwrite existing file: ${path}`);
37102
37303
  }
37103
- mkdirSync8(dirname8(path), { recursive: true });
37304
+ mkdirSync9(dirname8(path), { recursive: true });
37104
37305
  writeFileSync5(path, content, "utf8");
37105
37306
  };
37106
37307
  var deriveProjectName = (repoRoot) => {
37107
- const name = basename2(repoRoot).trim();
37308
+ const name = basename3(repoRoot).trim();
37108
37309
  return name || "Project";
37109
37310
  };
37110
37311
  var resolveRepoRoot = (startDir, schubRoot, repoRoot) => {
@@ -37196,7 +37397,7 @@ var runProjectCreate = (args, startDir) => {
37196
37397
  };
37197
37398
 
37198
37399
  // src/commands/review.ts
37199
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
37400
+ import { existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
37200
37401
  import { dirname as dirname9, join as join12, resolve as resolve7 } from "node:path";
37201
37402
  import { fileURLToPath as fileURLToPath8 } from "node:url";
37202
37403
  var BUNDLED_REVIEW_TEMPLATE_PATH = fileURLToPath8(new URL("../../templates/review-proposal/review-me-template.md", import.meta.url));
@@ -37330,7 +37531,7 @@ var runReviewCreate = (args, startDir) => {
37330
37531
  const schubDir = resolveChangeRoot(startDir);
37331
37532
  const trimmedId = options.changeId.trim();
37332
37533
  if (!isValidChangeId(trimmedId)) {
37333
- throw new Error(`Invalid change-id '${options.changeId}'. Use kebab-case or a C-prefixed id (e.g., C001_add-user-auth).`);
37534
+ throw new Error(`Invalid change-id '${options.changeId}'. Use kebab-case or a C-prefixed id (e.g., C0001_add-user-auth).`);
37334
37535
  }
37335
37536
  const changeId = normalizeChangeId(trimmedId);
37336
37537
  const changeTitle = options.title?.trim() || readChangeSummary(schubDir, changeId).changeTitle;
@@ -37338,7 +37539,7 @@ var runReviewCreate = (args, startDir) => {
37338
37539
  if (existsSync10(outputPath) && !options.overwrite) {
37339
37540
  throw new Error(`Refusing to overwrite existing file: ${outputPath}`);
37340
37541
  }
37341
- mkdirSync9(dirname9(outputPath), { recursive: true });
37542
+ mkdirSync10(dirname9(outputPath), { recursive: true });
37342
37543
  const templatePath = resolveTemplatePath(schubDir, join12("review-proposal", "review-me-template.md"), BUNDLED_REVIEW_TEMPLATE_PATH);
37343
37544
  const template = readFileSync8(templatePath, "utf8");
37344
37545
  const rendered = renderChangeTemplate(template, changeTitle || changeId, changeId);
@@ -37533,6 +37734,8 @@ var HELP_TEXT = `schub [command]
37533
37734
  Commands:
37534
37735
  changes create Create a change proposal
37535
37736
  changes status Update change proposal status
37737
+ changes archive Archive a change proposal
37738
+ changes list List change proposals
37536
37739
  project create Create project docs
37537
37740
  tasks create Create task files for a change
37538
37741
  tasks list List tasks
@@ -37582,6 +37785,14 @@ var runCommand = async () => {
37582
37785
  runChangesStatus(rest, getStartDir());
37583
37786
  return;
37584
37787
  }
37788
+ if (secondary === "archive") {
37789
+ runChangesArchive(rest, getStartDir());
37790
+ return;
37791
+ }
37792
+ if (secondary === "list") {
37793
+ runChangesList(rest, getStartDir());
37794
+ return;
37795
+ }
37585
37796
  break;
37586
37797
  case "project":
37587
37798
  if (secondary === "create") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schub",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "schub": "./src/index.ts"