@outcomeeng/spx 0.1.6 → 0.1.7

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LEAF_KIND,
3
3
  parseWorkItemName
4
- } from "./chunk-5L7CHFBC.js";
4
+ } from "./chunk-BQLLI7GS.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -17,19 +17,19 @@ async function initCommand(options = {}) {
17
17
  ["plugin", "marketplace", "list"],
18
18
  { cwd }
19
19
  );
20
- const exists = listOutput.includes("spx-claude");
20
+ const exists = listOutput.includes("outcomeeng");
21
21
  if (!exists) {
22
22
  await execa(
23
23
  "claude",
24
- ["plugin", "marketplace", "add", "simonheimlicher/spx-claude"],
24
+ ["plugin", "marketplace", "add", "outcomeeng/claude"],
25
25
  { cwd }
26
26
  );
27
- return "\u2713 spx-claude marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
27
+ return "\u2713 outcomeeng marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
28
28
  } else {
29
- await execa("claude", ["plugin", "marketplace", "update", "spx-claude"], {
29
+ await execa("claude", ["plugin", "marketplace", "update", "outcomeeng"], {
30
30
  cwd
31
31
  });
32
- return "\u2713 spx-claude marketplace updated successfully\n\nThe marketplace is now up to date.";
32
+ return "\u2713 outcomeeng marketplace updated successfully\n\nThe marketplace is now up to date.";
33
33
  }
34
34
  } catch (error) {
35
35
  if (error instanceof Error) {
@@ -643,7 +643,7 @@ Searched for: **/.claude/settings.local.json`;
643
643
 
644
644
  // src/domains/claude/index.ts
645
645
  function registerClaudeCommands(claudeCmd) {
646
- claudeCmd.command("init").description("Initialize or update spx-claude marketplace plugin").action(async () => {
646
+ claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
647
647
  try {
648
648
  const output = await initCommand({ cwd: process.cwd() });
649
649
  console.log(output);
@@ -704,7 +704,118 @@ var claudeDomain = {
704
704
 
705
705
  // src/commands/session/archive.ts
706
706
  import { mkdir, rename, stat } from "fs/promises";
707
- import { dirname, join as join2 } from "path";
707
+ import { dirname as dirname2, join as join2 } from "path";
708
+
709
+ // src/git/root.ts
710
+ import { execa as execa2 } from "execa";
711
+ import { dirname, isAbsolute, join, resolve } from "path";
712
+
713
+ // src/config/defaults.ts
714
+ var DEFAULT_CONFIG = {
715
+ specs: {
716
+ root: "specs",
717
+ work: {
718
+ dir: "work",
719
+ statusDirs: {
720
+ doing: "doing",
721
+ backlog: "backlog",
722
+ done: "archive"
723
+ }
724
+ },
725
+ decisions: "decisions",
726
+ templates: "templates"
727
+ },
728
+ sessions: {
729
+ dir: ".spx/sessions",
730
+ statusDirs: {
731
+ todo: "todo",
732
+ doing: "doing",
733
+ archive: "archive"
734
+ }
735
+ }
736
+ };
737
+
738
+ // src/git/root.ts
739
+ var defaultDeps = {
740
+ execa: async (command, args, options) => {
741
+ const result = await execa2(command, args, options);
742
+ return {
743
+ exitCode: result.exitCode ?? 0,
744
+ stdout: typeof result.stdout === "string" ? result.stdout : String(result.stdout),
745
+ stderr: typeof result.stderr === "string" ? result.stderr : String(result.stderr)
746
+ };
747
+ }
748
+ };
749
+ var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
750
+ function extractStdout(stdout) {
751
+ if (!stdout) return "";
752
+ const str = typeof stdout === "string" ? stdout : String(stdout);
753
+ return str.trim().replace(/\/+$/, "");
754
+ }
755
+ async function detectMainRepoRoot(cwd = process.cwd(), deps = defaultDeps) {
756
+ try {
757
+ const toplevelResult = await deps.execa(
758
+ "git",
759
+ ["rev-parse", "--show-toplevel"],
760
+ { cwd, reject: false }
761
+ );
762
+ if (toplevelResult.exitCode !== 0 || !toplevelResult.stdout) {
763
+ return {
764
+ root: cwd,
765
+ isGitRepo: false,
766
+ warning: NOT_GIT_REPO_WARNING
767
+ };
768
+ }
769
+ const toplevel = extractStdout(toplevelResult.stdout);
770
+ const commonDirResult = await deps.execa(
771
+ "git",
772
+ ["rev-parse", "--git-common-dir"],
773
+ { cwd, reject: false }
774
+ );
775
+ if (commonDirResult.exitCode !== 0 || !commonDirResult.stdout) {
776
+ return {
777
+ root: toplevel,
778
+ isGitRepo: true
779
+ };
780
+ }
781
+ const commonDir = extractStdout(commonDirResult.stdout);
782
+ const absoluteCommonDir = isAbsolute(commonDir) ? commonDir : resolve(toplevel, commonDir);
783
+ const mainRepoRoot = dirname(absoluteCommonDir);
784
+ return {
785
+ root: mainRepoRoot,
786
+ isGitRepo: true
787
+ };
788
+ } catch {
789
+ return {
790
+ root: cwd,
791
+ isGitRepo: false,
792
+ warning: NOT_GIT_REPO_WARNING
793
+ };
794
+ }
795
+ }
796
+ async function resolveSessionConfig(options = {}) {
797
+ const { sessionsDir, cwd, deps } = options;
798
+ const { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
799
+ if (sessionsDir) {
800
+ return {
801
+ config: {
802
+ todoDir: join(sessionsDir, statusDirs2.todo),
803
+ doingDir: join(sessionsDir, statusDirs2.doing),
804
+ archiveDir: join(sessionsDir, statusDirs2.archive)
805
+ }
806
+ };
807
+ }
808
+ const gitResult = await detectMainRepoRoot(cwd, deps);
809
+ const baseDir = join(gitResult.root, DEFAULT_CONFIG.sessions.dir);
810
+ return {
811
+ config: {
812
+ todoDir: join(baseDir, statusDirs2.todo),
813
+ doingDir: join(baseDir, statusDirs2.doing),
814
+ archiveDir: join(baseDir, statusDirs2.archive)
815
+ },
816
+ warning: gitResult.warning
817
+ };
818
+ }
708
819
 
709
820
  // src/session/errors.ts
710
821
  var SessionError = class extends Error {
@@ -753,33 +864,73 @@ var NoSessionsAvailableError = class extends SessionError {
753
864
  }
754
865
  };
755
866
 
756
- // src/session/show.ts
757
- import { join } from "path";
758
-
759
- // src/config/defaults.ts
760
- var DEFAULT_CONFIG = {
761
- specs: {
762
- root: "specs",
763
- work: {
764
- dir: "work",
765
- statusDirs: {
766
- doing: "doing",
767
- backlog: "backlog",
768
- done: "archive"
769
- }
770
- },
771
- decisions: "decisions",
772
- templates: "templates"
773
- },
774
- sessions: {
775
- dir: ".spx/sessions",
776
- statusDirs: {
777
- todo: "todo",
778
- doing: "doing",
779
- archive: "archive"
780
- }
867
+ // src/commands/session/archive.ts
868
+ var SessionAlreadyArchivedError = class extends Error {
869
+ /** The session ID that is already archived */
870
+ sessionId;
871
+ constructor(sessionId) {
872
+ super(`Session already archived: ${sessionId}.`);
873
+ this.name = "SessionAlreadyArchivedError";
874
+ this.sessionId = sessionId;
781
875
  }
782
876
  };
877
+ async function resolveArchivePaths(sessionId, config) {
878
+ const filename = `${sessionId}.md`;
879
+ const todoPath = join2(config.todoDir, filename);
880
+ const doingPath = join2(config.doingDir, filename);
881
+ const archivePath = join2(config.archiveDir, filename);
882
+ try {
883
+ const archiveStats = await stat(archivePath);
884
+ if (archiveStats.isFile()) {
885
+ throw new SessionAlreadyArchivedError(sessionId);
886
+ }
887
+ } catch (error) {
888
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
889
+ throw error;
890
+ }
891
+ if (error instanceof SessionAlreadyArchivedError) {
892
+ throw error;
893
+ }
894
+ }
895
+ try {
896
+ const todoStats = await stat(todoPath);
897
+ if (todoStats.isFile()) {
898
+ return { source: todoPath, target: archivePath };
899
+ }
900
+ } catch {
901
+ }
902
+ try {
903
+ const doingStats = await stat(doingPath);
904
+ if (doingStats.isFile()) {
905
+ return { source: doingPath, target: archivePath };
906
+ }
907
+ } catch {
908
+ }
909
+ throw new SessionNotFoundError(sessionId);
910
+ }
911
+ async function archiveCommand(options) {
912
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
913
+ const { source, target } = await resolveArchivePaths(options.sessionId, config);
914
+ await mkdir(dirname2(target), { recursive: true });
915
+ await rename(source, target);
916
+ return `Archived session: ${options.sessionId}
917
+ Archive location: ${target}`;
918
+ }
919
+
920
+ // src/commands/session/delete.ts
921
+ import { stat as stat2, unlink } from "fs/promises";
922
+
923
+ // src/session/delete.ts
924
+ function resolveDeletePath(sessionId, existingPaths) {
925
+ const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
926
+ if (!matchingPath) {
927
+ throw new SessionNotFoundError(sessionId);
928
+ }
929
+ return matchingPath;
930
+ }
931
+
932
+ // src/session/show.ts
933
+ import { join as join3 } from "path";
783
934
 
784
935
  // src/session/list.ts
785
936
  import { parse as parseYaml } from "yaml";
@@ -819,6 +970,8 @@ function parseSessionId(id) {
819
970
  }
820
971
 
821
972
  // src/session/types.ts
973
+ var SESSION_STATUSES = ["todo", "doing", "archive"];
974
+ var DEFAULT_LIST_STATUSES = ["doing", "todo"];
822
975
  var PRIORITY_ORDER = {
823
976
  high: 0,
824
977
  medium: 1,
@@ -905,11 +1058,11 @@ function sortSessions(sessions) {
905
1058
  // src/session/show.ts
906
1059
  var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
907
1060
  var DEFAULT_SESSION_CONFIG = {
908
- todoDir: join(sessionsBaseDir, statusDirs.todo),
909
- doingDir: join(sessionsBaseDir, statusDirs.doing),
910
- archiveDir: join(sessionsBaseDir, statusDirs.archive)
1061
+ todoDir: join3(sessionsBaseDir, statusDirs.todo),
1062
+ doingDir: join3(sessionsBaseDir, statusDirs.doing),
1063
+ archiveDir: join3(sessionsBaseDir, statusDirs.archive)
911
1064
  };
912
- var SEARCH_ORDER = ["todo", "doing", "archive"];
1065
+ var SEARCH_ORDER = [...SESSION_STATUSES];
913
1066
  function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
914
1067
  const filename = `${id}.md`;
915
1068
  return [
@@ -941,76 +1094,6 @@ function formatShowOutput(content, options) {
941
1094
  return header + separator + content;
942
1095
  }
943
1096
 
944
- // src/commands/session/archive.ts
945
- var SessionAlreadyArchivedError = class extends Error {
946
- /** The session ID that is already archived */
947
- sessionId;
948
- constructor(sessionId) {
949
- super(`Session already archived: ${sessionId}.`);
950
- this.name = "SessionAlreadyArchivedError";
951
- this.sessionId = sessionId;
952
- }
953
- };
954
- async function resolveArchivePaths(sessionId, config) {
955
- const filename = `${sessionId}.md`;
956
- const todoPath = join2(config.todoDir, filename);
957
- const doingPath = join2(config.doingDir, filename);
958
- const archivePath = join2(config.archiveDir, filename);
959
- try {
960
- const archiveStats = await stat(archivePath);
961
- if (archiveStats.isFile()) {
962
- throw new SessionAlreadyArchivedError(sessionId);
963
- }
964
- } catch (error) {
965
- if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
966
- throw error;
967
- }
968
- if (error instanceof SessionAlreadyArchivedError) {
969
- throw error;
970
- }
971
- }
972
- try {
973
- const todoStats = await stat(todoPath);
974
- if (todoStats.isFile()) {
975
- return { source: todoPath, target: archivePath };
976
- }
977
- } catch {
978
- }
979
- try {
980
- const doingStats = await stat(doingPath);
981
- if (doingStats.isFile()) {
982
- return { source: doingPath, target: archivePath };
983
- }
984
- } catch {
985
- }
986
- throw new SessionNotFoundError(sessionId);
987
- }
988
- async function archiveCommand(options) {
989
- const config = options.sessionsDir ? {
990
- todoDir: join2(options.sessionsDir, "todo"),
991
- doingDir: join2(options.sessionsDir, "doing"),
992
- archiveDir: join2(options.sessionsDir, "archive")
993
- } : DEFAULT_SESSION_CONFIG;
994
- const { source, target } = await resolveArchivePaths(options.sessionId, config);
995
- await mkdir(dirname(target), { recursive: true });
996
- await rename(source, target);
997
- return `Archived session: ${options.sessionId}
998
- Archive location: ${target}`;
999
- }
1000
-
1001
- // src/commands/session/delete.ts
1002
- import { stat as stat2, unlink } from "fs/promises";
1003
- import { join as join3 } from "path";
1004
-
1005
- // src/session/delete.ts
1006
- function resolveDeletePath(sessionId, existingPaths) {
1007
- const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
1008
- if (!matchingPath) {
1009
- throw new SessionNotFoundError(sessionId);
1010
- }
1011
- return matchingPath;
1012
- }
1013
-
1014
1097
  // src/commands/session/delete.ts
1015
1098
  async function findExistingPaths(paths) {
1016
1099
  const existing = [];
@@ -1026,11 +1109,7 @@ async function findExistingPaths(paths) {
1026
1109
  return existing;
1027
1110
  }
1028
1111
  async function deleteCommand(options) {
1029
- const config = options.sessionsDir ? {
1030
- todoDir: join3(options.sessionsDir, "todo"),
1031
- doingDir: join3(options.sessionsDir, "doing"),
1032
- archiveDir: join3(options.sessionsDir, "archive")
1033
- } : DEFAULT_SESSION_CONFIG;
1112
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1034
1113
  const paths = resolveSessionPaths(options.sessionId, config);
1035
1114
  const existingPaths = await findExistingPaths(paths);
1036
1115
  const pathToDelete = resolveDeletePath(options.sessionId, existingPaths);
@@ -1040,47 +1119,7 @@ async function deleteCommand(options) {
1040
1119
 
1041
1120
  // src/commands/session/handoff.ts
1042
1121
  import { mkdir as mkdir2, writeFile } from "fs/promises";
1043
- import { join as join5, resolve } from "path";
1044
-
1045
- // src/git/root.ts
1046
- import { execa as execa2 } from "execa";
1047
- import { join as join4 } from "path";
1048
- var defaultDeps = {
1049
- execa: (command, args, options) => execa2(command, args, options)
1050
- };
1051
- var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
1052
- async function detectGitRoot(cwd = process.cwd(), deps = defaultDeps) {
1053
- try {
1054
- const result = await deps.execa(
1055
- "git",
1056
- ["rev-parse", "--show-toplevel"],
1057
- { cwd, reject: false }
1058
- );
1059
- if (result.exitCode === 0 && result.stdout) {
1060
- const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout.toString();
1061
- const gitRoot = stdout.trim().replace(/\/+$/, "");
1062
- return {
1063
- root: gitRoot,
1064
- isGitRepo: true
1065
- };
1066
- }
1067
- return {
1068
- root: cwd,
1069
- isGitRepo: false,
1070
- warning: NOT_GIT_REPO_WARNING
1071
- };
1072
- } catch {
1073
- return {
1074
- root: cwd,
1075
- isGitRepo: false,
1076
- warning: NOT_GIT_REPO_WARNING
1077
- };
1078
- }
1079
- }
1080
- function buildSessionPathFromRoot(gitRoot, sessionId, config) {
1081
- const filename = `${sessionId}.md`;
1082
- return join4(gitRoot, config.todoDir, filename);
1083
- }
1122
+ import { join as join4, resolve as resolve2 } from "path";
1084
1123
 
1085
1124
  // src/session/create.ts
1086
1125
  var MIN_CONTENT_LENGTH = 1;
@@ -1101,7 +1140,6 @@ function validateSessionContent(content) {
1101
1140
  }
1102
1141
 
1103
1142
  // src/commands/session/handoff.ts
1104
- var { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
1105
1143
  var FRONT_MATTER_START = /^---\r?\n/;
1106
1144
  function hasFrontmatter(content) {
1107
1145
  return FRONT_MATTER_START.test(content);
@@ -1126,20 +1164,7 @@ priority: medium
1126
1164
  ${content}`;
1127
1165
  }
1128
1166
  async function handoffCommand(options) {
1129
- const config = options.sessionsDir ? {
1130
- todoDir: join5(options.sessionsDir, statusDirs2.todo),
1131
- doingDir: join5(options.sessionsDir, statusDirs2.doing),
1132
- archiveDir: join5(options.sessionsDir, statusDirs2.archive)
1133
- } : DEFAULT_SESSION_CONFIG;
1134
- let baseDir;
1135
- let warningMessage;
1136
- if (options.sessionsDir) {
1137
- baseDir = options.sessionsDir;
1138
- } else {
1139
- const gitResult = await detectGitRoot();
1140
- baseDir = gitResult.root;
1141
- warningMessage = gitResult.warning;
1142
- }
1167
+ const { config, warning } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1143
1168
  const sessionId = generateSessionId();
1144
1169
  const fullContent = buildSessionContent(options.content);
1145
1170
  const validation = validateSessionContent(fullContent);
@@ -1147,23 +1172,21 @@ async function handoffCommand(options) {
1147
1172
  throw new SessionInvalidContentError(validation.error ?? "Unknown validation error");
1148
1173
  }
1149
1174
  const filename = `${sessionId}.md`;
1150
- const sessionPath = options.sessionsDir ? join5(config.todoDir, filename) : buildSessionPathFromRoot(baseDir, sessionId, config);
1151
- const absolutePath = resolve(sessionPath);
1152
- const todoDir = options.sessionsDir ? config.todoDir : join5(baseDir, config.todoDir);
1153
- await mkdir2(todoDir, { recursive: true });
1175
+ const sessionPath = join4(config.todoDir, filename);
1176
+ const absolutePath = resolve2(sessionPath);
1177
+ await mkdir2(config.todoDir, { recursive: true });
1154
1178
  await writeFile(sessionPath, fullContent, "utf-8");
1155
- let output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
1156
- <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
1157
- if (warningMessage) {
1158
- process.stderr.write(`${warningMessage}
1179
+ if (warning) {
1180
+ process.stderr.write(`${warning}
1159
1181
  `);
1160
1182
  }
1161
- return output;
1183
+ return `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
1184
+ <SESSION_FILE>${absolutePath}</SESSION_FILE>`;
1162
1185
  }
1163
1186
 
1164
1187
  // src/commands/session/list.ts
1165
1188
  import { readdir, readFile } from "fs/promises";
1166
- import { join as join6 } from "path";
1189
+ import { join as join5 } from "path";
1167
1190
  async function loadSessionsFromDir(dir, status) {
1168
1191
  try {
1169
1192
  const files = await readdir(dir);
@@ -1171,7 +1194,7 @@ async function loadSessionsFromDir(dir, status) {
1171
1194
  for (const file of files) {
1172
1195
  if (!file.endsWith(".md")) continue;
1173
1196
  const id = file.replace(".md", "");
1174
- const filePath = join6(dir, file);
1197
+ const filePath = join5(dir, file);
1175
1198
  const content = await readFile(filePath, "utf-8");
1176
1199
  const metadata = parseSessionMetadata(content);
1177
1200
  sessions.push({
@@ -1189,7 +1212,12 @@ async function loadSessionsFromDir(dir, status) {
1189
1212
  throw error;
1190
1213
  }
1191
1214
  }
1192
- function formatTextOutput(sessions, _status) {
1215
+ var STATUS_DIR_KEY = {
1216
+ todo: "todoDir",
1217
+ doing: "doingDir",
1218
+ archive: "archiveDir"
1219
+ };
1220
+ function formatTextOutput(sessions) {
1193
1221
  if (sessions.length === 0) {
1194
1222
  return ` (no sessions)`;
1195
1223
  }
@@ -1199,47 +1227,38 @@ function formatTextOutput(sessions, _status) {
1199
1227
  return ` ${s.id}${priority}${tags}`;
1200
1228
  }).join("\n");
1201
1229
  }
1230
+ function validateStatus(input) {
1231
+ if (SESSION_STATUSES.includes(input)) {
1232
+ return input;
1233
+ }
1234
+ throw new Error(
1235
+ `Invalid status: "${input}". Valid values: ${SESSION_STATUSES.join(", ")}`
1236
+ );
1237
+ }
1202
1238
  async function listCommand(options) {
1203
- const config = options.sessionsDir ? {
1204
- todoDir: join6(options.sessionsDir, "todo"),
1205
- doingDir: join6(options.sessionsDir, "doing"),
1206
- archiveDir: join6(options.sessionsDir, "archive")
1207
- } : DEFAULT_SESSION_CONFIG;
1208
- const statuses = options.status ? [options.status] : ["todo", "doing", "archive"];
1209
- const allSessions = {
1210
- todo: [],
1211
- doing: [],
1212
- archive: []
1213
- };
1239
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1240
+ const statuses = options.status !== void 0 ? [validateStatus(options.status)] : DEFAULT_LIST_STATUSES;
1241
+ const sessionsByStatus = {};
1214
1242
  for (const status of statuses) {
1215
- const dir = status === "todo" ? config.todoDir : status === "doing" ? config.doingDir : config.archiveDir;
1216
- const sessions = await loadSessionsFromDir(dir, status);
1217
- allSessions[status] = sortSessions(sessions);
1243
+ const dirKey = STATUS_DIR_KEY[status];
1244
+ const sessions = await loadSessionsFromDir(config[dirKey], status);
1245
+ sessionsByStatus[status] = sortSessions(sessions);
1218
1246
  }
1219
1247
  if (options.format === "json") {
1220
- return JSON.stringify(allSessions, null, 2);
1248
+ return JSON.stringify(sessionsByStatus, null, 2);
1221
1249
  }
1222
1250
  const lines = [];
1223
- if (statuses.includes("doing")) {
1224
- lines.push("DOING:");
1225
- lines.push(formatTextOutput(allSessions.doing, "doing"));
1226
- lines.push("");
1227
- }
1228
- if (statuses.includes("todo")) {
1229
- lines.push("TODO:");
1230
- lines.push(formatTextOutput(allSessions.todo, "todo"));
1251
+ for (const status of statuses) {
1252
+ lines.push(`${status.toUpperCase()}:`);
1253
+ lines.push(formatTextOutput(sessionsByStatus[status] ?? []));
1231
1254
  lines.push("");
1232
1255
  }
1233
- if (statuses.includes("archive")) {
1234
- lines.push("ARCHIVE:");
1235
- lines.push(formatTextOutput(allSessions.archive, "archive"));
1236
- }
1237
1256
  return lines.join("\n").trim();
1238
1257
  }
1239
1258
 
1240
1259
  // src/commands/session/pickup.ts
1241
1260
  import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rename as rename2 } from "fs/promises";
1242
- import { join as join7 } from "path";
1261
+ import { join as join6 } from "path";
1243
1262
 
1244
1263
  // src/session/pickup.ts
1245
1264
  function buildClaimPaths(sessionId, config) {
@@ -1275,6 +1294,8 @@ function selectBestSession(sessions) {
1275
1294
  }
1276
1295
 
1277
1296
  // src/commands/session/pickup.ts
1297
+ var PICKUP_SOURCE_STATUS = SESSION_STATUSES[0];
1298
+ var PICKUP_TARGET_STATUS = SESSION_STATUSES[1];
1278
1299
  async function loadTodoSessions(config) {
1279
1300
  try {
1280
1301
  const files = await readdir2(config.todoDir);
@@ -1282,12 +1303,12 @@ async function loadTodoSessions(config) {
1282
1303
  for (const file of files) {
1283
1304
  if (!file.endsWith(".md")) continue;
1284
1305
  const id = file.replace(".md", "");
1285
- const filePath = join7(config.todoDir, file);
1306
+ const filePath = join6(config.todoDir, file);
1286
1307
  const content = await readFile2(filePath, "utf-8");
1287
1308
  const metadata = parseSessionMetadata(content);
1288
1309
  sessions.push({
1289
1310
  id,
1290
- status: "todo",
1311
+ status: PICKUP_SOURCE_STATUS,
1291
1312
  path: filePath,
1292
1313
  metadata
1293
1314
  });
@@ -1301,11 +1322,7 @@ async function loadTodoSessions(config) {
1301
1322
  }
1302
1323
  }
1303
1324
  async function pickupCommand(options) {
1304
- const config = options.sessionsDir ? {
1305
- todoDir: join7(options.sessionsDir, "todo"),
1306
- doingDir: join7(options.sessionsDir, "doing"),
1307
- archiveDir: join7(options.sessionsDir, "archive")
1308
- } : DEFAULT_SESSION_CONFIG;
1325
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1309
1326
  let sessionId;
1310
1327
  if (options.auto) {
1311
1328
  const sessions = await loadTodoSessions(config);
@@ -1327,7 +1344,7 @@ async function pickupCommand(options) {
1327
1344
  throw classifyClaimError(error, sessionId);
1328
1345
  }
1329
1346
  const content = await readFile2(paths.target, "utf-8");
1330
- const output = formatShowOutput(content, { status: "doing" });
1347
+ const output = formatShowOutput(content, { status: PICKUP_TARGET_STATUS });
1331
1348
  return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
1332
1349
 
1333
1350
  ${output}`;
@@ -1335,7 +1352,8 @@ ${output}`;
1335
1352
 
1336
1353
  // src/commands/session/prune.ts
1337
1354
  import { readdir as readdir3, readFile as readFile3, unlink as unlink2 } from "fs/promises";
1338
- import { join as join8 } from "path";
1355
+ import { join as join7 } from "path";
1356
+ var PRUNE_STATUS = SESSION_STATUSES[2];
1339
1357
  var DEFAULT_KEEP_COUNT = 5;
1340
1358
  var PruneValidationError = class extends Error {
1341
1359
  constructor(message) {
@@ -1359,12 +1377,12 @@ async function loadArchiveSessions(config) {
1359
1377
  for (const file of files) {
1360
1378
  if (!file.endsWith(".md")) continue;
1361
1379
  const id = file.replace(".md", "");
1362
- const filePath = join8(config.archiveDir, file);
1380
+ const filePath = join7(config.archiveDir, file);
1363
1381
  const content = await readFile3(filePath, "utf-8");
1364
1382
  const metadata = parseSessionMetadata(content);
1365
1383
  sessions.push({
1366
1384
  id,
1367
- status: "archive",
1385
+ status: PRUNE_STATUS,
1368
1386
  path: filePath,
1369
1387
  metadata
1370
1388
  });
@@ -1388,11 +1406,7 @@ async function pruneCommand(options) {
1388
1406
  validatePruneOptions(options);
1389
1407
  const keep = options.keep ?? DEFAULT_KEEP_COUNT;
1390
1408
  const dryRun = options.dryRun ?? false;
1391
- const config = options.sessionsDir ? {
1392
- todoDir: join8(options.sessionsDir, "todo"),
1393
- doingDir: join8(options.sessionsDir, "doing"),
1394
- archiveDir: join8(options.sessionsDir, "archive")
1395
- } : DEFAULT_SESSION_CONFIG;
1409
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1396
1410
  const sessions = await loadArchiveSessions(config);
1397
1411
  const toPrune = selectSessionsToPrune(sessions, keep);
1398
1412
  if (toPrune.length === 0) {
@@ -1421,7 +1435,6 @@ async function pruneCommand(options) {
1421
1435
 
1422
1436
  // src/commands/session/release.ts
1423
1437
  import { readdir as readdir4, rename as rename3 } from "fs/promises";
1424
- import { join as join9 } from "path";
1425
1438
 
1426
1439
  // src/session/release.ts
1427
1440
  function buildReleasePaths(sessionId, config) {
@@ -1458,11 +1471,7 @@ async function loadDoingSessions(config) {
1458
1471
  }
1459
1472
  }
1460
1473
  async function releaseCommand(options) {
1461
- const config = options.sessionsDir ? {
1462
- todoDir: join9(options.sessionsDir, "todo"),
1463
- doingDir: join9(options.sessionsDir, "doing"),
1464
- archiveDir: join9(options.sessionsDir, "archive")
1465
- } : DEFAULT_SESSION_CONFIG;
1474
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1466
1475
  let sessionId;
1467
1476
  if (options.sessionId) {
1468
1477
  sessionId = options.sessionId;
@@ -1489,7 +1498,6 @@ Session returned to todo directory.`;
1489
1498
 
1490
1499
  // src/commands/session/show.ts
1491
1500
  import { readFile as readFile4, stat as stat3 } from "fs/promises";
1492
- import { join as join10 } from "path";
1493
1501
  async function findExistingPath(paths, _config) {
1494
1502
  for (let i = 0; i < paths.length; i++) {
1495
1503
  const filePath = paths[i];
@@ -1504,11 +1512,7 @@ async function findExistingPath(paths, _config) {
1504
1512
  return null;
1505
1513
  }
1506
1514
  async function showCommand(options) {
1507
- const config = options.sessionsDir ? {
1508
- todoDir: join10(options.sessionsDir, "todo"),
1509
- doingDir: join10(options.sessionsDir, "doing"),
1510
- archiveDir: join10(options.sessionsDir, "archive")
1511
- } : DEFAULT_SESSION_CONFIG;
1515
+ const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
1512
1516
  const paths = resolveSessionPaths(options.sessionId, config);
1513
1517
  const found = await findExistingPath(paths, config);
1514
1518
  if (!found) {
@@ -1580,17 +1584,17 @@ async function readStdin() {
1580
1584
  if (process.stdin.isTTY) {
1581
1585
  return void 0;
1582
1586
  }
1583
- return new Promise((resolve2) => {
1587
+ return new Promise((resolve3) => {
1584
1588
  let data = "";
1585
1589
  process.stdin.setEncoding("utf-8");
1586
1590
  process.stdin.on("data", (chunk) => {
1587
1591
  data += chunk;
1588
1592
  });
1589
1593
  process.stdin.on("end", () => {
1590
- resolve2(data.trim() || void 0);
1594
+ resolve3(data.trim() || void 0);
1591
1595
  });
1592
1596
  process.stdin.on("error", () => {
1593
- resolve2(void 0);
1597
+ resolve3(void 0);
1594
1598
  });
1595
1599
  });
1596
1600
  }
@@ -1599,7 +1603,7 @@ function handleError(error) {
1599
1603
  process.exit(1);
1600
1604
  }
1601
1605
  function registerSessionCommands(sessionCmd) {
1602
- sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status (todo|doing|archive)").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1606
+ sessionCmd.command("list").description("List active sessions (doing + todo by default)").option("--status <status>", "Filter by status (todo|doing|archive); defaults to doing + todo").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1603
1607
  try {
1604
1608
  const output = await listCommand({
1605
1609
  status: options.status,
@@ -1611,6 +1615,18 @@ function registerSessionCommands(sessionCmd) {
1611
1615
  handleError(error);
1612
1616
  }
1613
1617
  });
1618
+ sessionCmd.command("todo").description("List todo sessions").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
1619
+ try {
1620
+ const output = await listCommand({
1621
+ status: SESSION_STATUSES[0],
1622
+ format: options.json ? "json" : "text",
1623
+ sessionsDir: options.sessionsDir
1624
+ });
1625
+ console.log(output);
1626
+ } catch (error) {
1627
+ handleError(error);
1628
+ }
1629
+ });
1614
1630
  sessionCmd.command("show <id>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
1615
1631
  try {
1616
1632
  const output = await showCommand({
@@ -3182,7 +3198,7 @@ var ParseErrorCode;
3182
3198
 
3183
3199
  // src/validation/config/scope.ts
3184
3200
  import { existsSync, readdirSync, readFileSync } from "fs";
3185
- import { join as join11 } from "path";
3201
+ import { join as join8 } from "path";
3186
3202
  var TSCONFIG_FILES = {
3187
3203
  full: "tsconfig.json",
3188
3204
  production: "tsconfig.production.json"
@@ -3229,7 +3245,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
3229
3245
  if (hasDirectTsFiles) return true;
3230
3246
  const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
3231
3247
  for (const subdir of subdirs.slice(0, 5)) {
3232
- if (hasTypeScriptFilesRecursive(join11(dirPath, subdir.name), maxDepth - 1, deps)) {
3248
+ if (hasTypeScriptFilesRecursive(join8(dirPath, subdir.name), maxDepth - 1, deps)) {
3233
3249
  return true;
3234
3250
  }
3235
3251
  }
@@ -3572,7 +3588,7 @@ function buildEslintArgs(context) {
3572
3588
  }
3573
3589
  async function validateESLint(context, runner = defaultEslintProcessRunner) {
3574
3590
  const { scope, validatedFiles, mode } = context;
3575
- return new Promise((resolve2) => {
3591
+ return new Promise((resolve3) => {
3576
3592
  if (!validatedFiles || validatedFiles.length === 0) {
3577
3593
  if (scope === VALIDATION_SCOPES.PRODUCTION) {
3578
3594
  process.env.ESLINT_PRODUCTION_ONLY = "1";
@@ -3591,13 +3607,13 @@ async function validateESLint(context, runner = defaultEslintProcessRunner) {
3591
3607
  });
3592
3608
  eslintProcess.on("close", (code) => {
3593
3609
  if (code === 0) {
3594
- resolve2({ success: true });
3610
+ resolve3({ success: true });
3595
3611
  } else {
3596
- resolve2({ success: false, error: `ESLint exited with code ${code}` });
3612
+ resolve3({ success: false, error: `ESLint exited with code ${code}` });
3597
3613
  }
3598
3614
  });
3599
3615
  eslintProcess.on("error", (error) => {
3600
- resolve2({ success: false, error: error.message });
3616
+ resolve3({ success: false, error: error.message });
3601
3617
  });
3602
3618
  });
3603
3619
  }
@@ -3644,7 +3660,7 @@ async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner)
3644
3660
  if (analyzeDirectories.length === 0) {
3645
3661
  return { success: true };
3646
3662
  }
3647
- return new Promise((resolve2) => {
3663
+ return new Promise((resolve3) => {
3648
3664
  const knipProcess = runner.spawn("npx", ["knip"], {
3649
3665
  cwd: process.cwd(),
3650
3666
  stdio: "pipe"
@@ -3659,17 +3675,17 @@ async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner)
3659
3675
  });
3660
3676
  knipProcess.on("close", (code) => {
3661
3677
  if (code === 0) {
3662
- resolve2({ success: true });
3678
+ resolve3({ success: true });
3663
3679
  } else {
3664
3680
  const errorOutput = knipOutput || knipError || "Unused code detected";
3665
- resolve2({
3681
+ resolve3({
3666
3682
  success: false,
3667
3683
  error: errorOutput
3668
3684
  });
3669
3685
  }
3670
3686
  });
3671
3687
  knipProcess.on("error", (error) => {
3672
- resolve2({ success: false, error: error.message });
3688
+ resolve3({ success: false, error: error.message });
3673
3689
  });
3674
3690
  });
3675
3691
  } catch (error) {
@@ -3761,7 +3777,7 @@ import { spawn as spawn3 } from "child_process";
3761
3777
  import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "fs";
3762
3778
  import { mkdtemp } from "fs/promises";
3763
3779
  import { tmpdir } from "os";
3764
- import { isAbsolute, join as join12 } from "path";
3780
+ import { isAbsolute as isAbsolute2, join as join9 } from "path";
3765
3781
  var defaultTypeScriptProcessRunner = { spawn: spawn3 };
3766
3782
  var defaultTypeScriptDeps = {
3767
3783
  mkdtemp,
@@ -3775,19 +3791,19 @@ function buildTypeScriptArgs(context) {
3775
3791
  return scope === VALIDATION_SCOPES.FULL ? ["tsc", "--noEmit"] : ["tsc", "--project", configFile];
3776
3792
  }
3777
3793
  async function createFileSpecificTsconfig(scope, files, deps = defaultTypeScriptDeps) {
3778
- const tempDir = await deps.mkdtemp(join12(tmpdir(), "validate-ts-"));
3779
- const configPath = join12(tempDir, "tsconfig.json");
3794
+ const tempDir = await deps.mkdtemp(join9(tmpdir(), "validate-ts-"));
3795
+ const configPath = join9(tempDir, "tsconfig.json");
3780
3796
  const baseConfigFile = TSCONFIG_FILES[scope];
3781
3797
  const projectRoot = process.cwd();
3782
- const absoluteFiles = files.map((file) => isAbsolute(file) ? file : join12(projectRoot, file));
3798
+ const absoluteFiles = files.map((file) => isAbsolute2(file) ? file : join9(projectRoot, file));
3783
3799
  const tempConfig = {
3784
- extends: join12(projectRoot, baseConfigFile),
3800
+ extends: join9(projectRoot, baseConfigFile),
3785
3801
  files: absoluteFiles,
3786
3802
  include: [],
3787
3803
  exclude: [],
3788
3804
  compilerOptions: {
3789
3805
  noEmit: true,
3790
- typeRoots: [join12(projectRoot, "node_modules", "@types")],
3806
+ typeRoots: [join9(projectRoot, "node_modules", "@types")],
3791
3807
  types: ["node"]
3792
3808
  }
3793
3809
  };
@@ -3807,7 +3823,7 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3807
3823
  if (files && files.length > 0) {
3808
3824
  const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, deps);
3809
3825
  try {
3810
- return await new Promise((resolve2) => {
3826
+ return await new Promise((resolve3) => {
3811
3827
  const tscProcess = runner.spawn("npx", ["tsc", "--project", configPath], {
3812
3828
  cwd: process.cwd(),
3813
3829
  stdio: "inherit"
@@ -3815,14 +3831,14 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3815
3831
  tscProcess.on("close", (code) => {
3816
3832
  cleanup();
3817
3833
  if (code === 0) {
3818
- resolve2({ success: true, skipped: false });
3834
+ resolve3({ success: true, skipped: false });
3819
3835
  } else {
3820
- resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3836
+ resolve3({ success: false, error: `TypeScript exited with code ${code}` });
3821
3837
  }
3822
3838
  });
3823
3839
  tscProcess.on("error", (error) => {
3824
3840
  cleanup();
3825
- resolve2({ success: false, error: error.message });
3841
+ resolve3({ success: false, error: error.message });
3826
3842
  });
3827
3843
  });
3828
3844
  } catch (error) {
@@ -3834,20 +3850,20 @@ async function validateTypeScript(scope, typescriptScope, files, runner = defaul
3834
3850
  tool = "npx";
3835
3851
  tscArgs = buildTypeScriptArgs({ scope, configFile });
3836
3852
  }
3837
- return new Promise((resolve2) => {
3853
+ return new Promise((resolve3) => {
3838
3854
  const tscProcess = runner.spawn(tool, tscArgs, {
3839
3855
  cwd: process.cwd(),
3840
3856
  stdio: "inherit"
3841
3857
  });
3842
3858
  tscProcess.on("close", (code) => {
3843
3859
  if (code === 0) {
3844
- resolve2({ success: true, skipped: false });
3860
+ resolve3({ success: true, skipped: false });
3845
3861
  } else {
3846
- resolve2({ success: false, error: `TypeScript exited with code ${code}` });
3862
+ resolve3({ success: false, error: `TypeScript exited with code ${code}` });
3847
3863
  }
3848
3864
  });
3849
3865
  tscProcess.on("error", (error) => {
3850
- resolve2({ success: false, error: error.message });
3866
+ resolve3({ success: false, error: error.message });
3851
3867
  });
3852
3868
  });
3853
3869
  }