@jskit-ai/jskit-cli 0.2.96 → 0.2.97

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 (31) hide show
  1. package/package.json +4 -4
  2. package/src/server/appBlueprint.js +37 -14
  3. package/src/server/core/argParser.js +0 -12
  4. package/src/server/core/commandCatalog.js +2 -92
  5. package/src/server/core/createCommandHandlers.js +0 -3
  6. package/src/server/index.js +0 -1
  7. package/src/server/{sessionRuntime/prompts → prompts}/app_blueprint.md +1 -1
  8. package/src/server/commandHandlers/session.js +0 -471
  9. package/src/server/sessionRuntime/appReadiness.js +0 -55
  10. package/src/server/sessionRuntime/constants.js +0 -377
  11. package/src/server/sessionRuntime/io.js +0 -97
  12. package/src/server/sessionRuntime/paths.js +0 -163
  13. package/src/server/sessionRuntime/preconditions.js +0 -663
  14. package/src/server/sessionRuntime/promptRenderer.js +0 -41
  15. package/src/server/sessionRuntime/prompts/automated_checks_run.md +0 -28
  16. package/src/server/sessionRuntime/prompts/blueprint_updated.md +0 -29
  17. package/src/server/sessionRuntime/prompts/deep_ui_check_run.md +0 -40
  18. package/src/server/sessionRuntime/prompts/final_comment.md +0 -10
  19. package/src/server/sessionRuntime/prompts/final_report_created.md +0 -44
  20. package/src/server/sessionRuntime/prompts/issue_created.md +0 -26
  21. package/src/server/sessionRuntime/prompts/issue_prompt_rendered.md +0 -1
  22. package/src/server/sessionRuntime/prompts/make_plan.md +0 -57
  23. package/src/server/sessionRuntime/prompts/plan_executed.md +0 -39
  24. package/src/server/sessionRuntime/prompts/pr_failure.md +0 -28
  25. package/src/server/sessionRuntime/prompts/pr_merge_prepared.md +0 -22
  26. package/src/server/sessionRuntime/prompts/review_changes_accepted_resolve.md +0 -12
  27. package/src/server/sessionRuntime/prompts/review_prompt_rendered.md +0 -61
  28. package/src/server/sessionRuntime/prompts/user_check_completed.md +0 -17
  29. package/src/server/sessionRuntime/responses.js +0 -1481
  30. package/src/server/sessionRuntime/worktrees.js +0 -31
  31. package/src/server/sessionRuntime.js +0 -3659
@@ -1,377 +0,0 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
-
4
- const SESSION_STATUS = Object.freeze({
5
- ABANDONED: "abandoned",
6
- BLOCKED: "blocked",
7
- FAILED: "failed",
8
- FINISHED: "finished",
9
- PENDING: "pending",
10
- RUNNING: "running",
11
- WAITING_FOR_USER: "waiting_for_user"
12
- });
13
-
14
- const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:-[a-z0-9]{4})?$/u;
15
- const SESSION_STATE_RELATIVE_PATH = ".jskit/sessions";
16
- const SESSION_WORKFLOW_VERSION = "7";
17
- const DEPENDENCIES_INSTALL_RESULT_FILE = "dependencies_install_result";
18
- const REVIEW_PASS_LIMIT = 0;
19
- const PROMPT_DIRECTORY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "prompts");
20
- const JSKIT_CLI_SHELL_COMMAND = "npx --no-install jskit";
21
- const DEFAULT_NEXT_COMMAND_TEMPLATE = `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} next`;
22
-
23
- const INPUT_NONE = Object.freeze({ type: "none" });
24
- const USER_CHECK_INPUT = Object.freeze({
25
- label: "Choose user check result",
26
- name: "userCheck",
27
- options: Object.freeze([
28
- Object.freeze({ label: "Passed", value: "passed" }),
29
- Object.freeze({ label: "Failed", value: "failed" })
30
- ]),
31
- required: true,
32
- type: "choice"
33
- });
34
-
35
- function codexHandoff({
36
- promptActionLabel = "",
37
- promptIntroText = "",
38
- promptWaitingText = "",
39
- sendPrompt = false
40
- } = {}) {
41
- return Object.freeze({
42
- mode: "inject_prompt",
43
- promptField: "prompt",
44
- ...(promptActionLabel ? { promptActionLabel } : {}),
45
- ...(promptIntroText ? { promptIntroText } : {}),
46
- ...(promptWaitingText ? { promptWaitingText } : {}),
47
- ...(sendPrompt ? { sendPrompt: true } : {})
48
- });
49
- }
50
-
51
- function stepAutomationFor({
52
- codex = undefined,
53
- id,
54
- kind,
55
- requiresExplicitRun = false
56
- }) {
57
- if (requiresExplicitRun) {
58
- return Object.freeze({ mode: "manual" });
59
- }
60
- if (id === "dependencies_installed") {
61
- return Object.freeze({ mode: "terminal" });
62
- }
63
- if (kind === "automatic") {
64
- return Object.freeze({ mode: "manual" });
65
- }
66
- if (kind === "codex_prompt" && codex?.sendPrompt === true) {
67
- return Object.freeze({ mode: "codex_prompt" });
68
- }
69
- return Object.freeze({ mode: "manual" });
70
- }
71
-
72
- const ISSUE_DEFINITION_CODEX_HANDOFF = codexHandoff({
73
- promptActionLabel: "Define issue",
74
- sendPrompt: true
75
- });
76
- const ISSUE_FILE_CODEX_HANDOFF = codexHandoff({
77
- promptActionLabel: "Create issue file",
78
- sendPrompt: true
79
- });
80
- const PLAN_CODEX_HANDOFF = codexHandoff({
81
- promptActionLabel: "Make plan",
82
- sendPrompt: true
83
- });
84
- const PLAN_EXECUTION_CODEX_HANDOFF = codexHandoff({
85
- promptActionLabel: "Execute plan",
86
- sendPrompt: true
87
- });
88
- const REVIEW_EXECUTION_CODEX_HANDOFF = codexHandoff({
89
- promptActionLabel: "Run deslop",
90
- sendPrompt: true
91
- });
92
- const RESOLVE_DESLOP_CODEX_HANDOFF = codexHandoff({
93
- promptActionLabel: "Resolve deslop",
94
- sendPrompt: true
95
- });
96
- const DEEP_UI_CHECK_CODEX_HANDOFF = codexHandoff({
97
- promptActionLabel: "Run deep UI check",
98
- sendPrompt: true
99
- });
100
- const AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF = codexHandoff({
101
- promptActionLabel: "Run automated checks",
102
- sendPrompt: true
103
- });
104
- const BLUEPRINT_CODEX_HANDOFF = codexHandoff({
105
- promptActionLabel: "Update blueprint",
106
- sendPrompt: true
107
- });
108
- const PR_FILE_CODEX_HANDOFF = codexHandoff({
109
- promptActionLabel: "Create PR file",
110
- sendPrompt: true
111
- });
112
- const PR_MERGE_PREP_CODEX_HANDOFF = codexHandoff({
113
- promptActionLabel: "Prepare PR merge",
114
- promptWaitingText: "Codex is preparing the PR for merge. Continue only when you decide the PR is ready.",
115
- sendPrompt: true
116
- });
117
-
118
- function defineStep({
119
- buttonLabel,
120
- codex = undefined,
121
- description,
122
- id,
123
- input = INPUT_NONE,
124
- kind = "automatic",
125
- label,
126
- nextCommandTemplate = DEFAULT_NEXT_COMMAND_TEMPLATE,
127
- preconditions = [],
128
- requiresExplicitRun = false,
129
- submitOptions = {},
130
- automation = undefined,
131
- utilityActions = [],
132
- displayGroupId = "",
133
- displayGroupLabel = ""
134
- }) {
135
- const resolvedAutomation = automation || stepAutomationFor({
136
- codex,
137
- id,
138
- kind,
139
- requiresExplicitRun
140
- });
141
- return Object.freeze({
142
- automation: Object.freeze({ ...resolvedAutomation }),
143
- buttonLabel,
144
- codex,
145
- description,
146
- displayGroupId,
147
- displayGroupLabel,
148
- id,
149
- input,
150
- kind,
151
- label,
152
- nextCommandTemplate,
153
- preconditions: Object.freeze([...preconditions]),
154
- requiresExplicitRun,
155
- submitOptions: Object.freeze({ ...submitOptions }),
156
- utilityActions: Object.freeze([...utilityActions])
157
- });
158
- }
159
-
160
- const STEP_DEFINITIONS = Object.freeze([
161
- defineStep({
162
- buttonLabel: "Create worktree",
163
- description: "JSKIT creates the isolated Git branch and session worktree where Codex will work.",
164
- id: "worktree_created",
165
- label: "Create worktree",
166
- preconditions: ["session_exists", "git_repository", "git_current_branch"]
167
- }),
168
- defineStep({
169
- buttonLabel: "Install dependencies",
170
- description: "JSKIT installs Node dependencies inside the session worktree before Codex starts.",
171
- id: "dependencies_installed",
172
- label: "Install dependencies",
173
- preconditions: ["session_exists", "worktree_exists"]
174
- }),
175
- defineStep({
176
- buttonLabel: "Define issue",
177
- description: "User describes the requested change; Codex helps scope and define the issue in the terminal.",
178
- displayGroupId: "define_create_issue",
179
- displayGroupLabel: "Define issue and create file",
180
- id: "issue_prompt_rendered",
181
- input: Object.freeze({
182
- label: "What should change?",
183
- multiline: true,
184
- name: "prompt",
185
- placeholder: "Describe the feature, bug, or change request.",
186
- required: true,
187
- type: "text"
188
- }),
189
- kind: "human_input",
190
- label: "Define issue",
191
- nextCommandTemplate: DEFAULT_NEXT_COMMAND_TEMPLATE,
192
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app"]
193
- }),
194
- defineStep({
195
- buttonLabel: "Create issue file",
196
- description: "JSKIT renders the issue-file prompt; Codex writes issue.md and issue_title for review.",
197
- displayGroupId: "define_create_issue",
198
- displayGroupLabel: "Define issue and create file",
199
- id: "issue_created",
200
- label: "Create issue file",
201
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app"],
202
- requiresExplicitRun: false
203
- }),
204
- defineStep({
205
- buttonLabel: "Create issue on GH",
206
- description: "JSKIT creates the GitHub issue from the reviewed issue files and records the issue metadata.",
207
- id: "issue_submitted",
208
- label: "Edit and submit issue",
209
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_text_exists"],
210
- requiresExplicitRun: false
211
- }),
212
- defineStep({
213
- buttonLabel: "Make plan",
214
- codex: PLAN_CODEX_HANDOFF,
215
- description: "Codex writes the plan in the terminal; JSKIT records it when the user continues.",
216
- id: "plan_made",
217
- kind: "codex_prompt",
218
- label: "Make plan",
219
- nextCommandTemplate: `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} next`,
220
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_text_exists", "issue_url_exists"]
221
- }),
222
- defineStep({
223
- buttonLabel: "Execute plan",
224
- codex: PLAN_EXECUTION_CODEX_HANDOFF,
225
- description: "JSKIT sends the plan to Codex; Codex implements it; the user advances after reviewing completion.",
226
- id: "plan_executed",
227
- kind: "codex_prompt",
228
- label: "Execute plan",
229
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_text_exists", "issue_url_exists"]
230
- }),
231
- defineStep({
232
- buttonLabel: "Run deep UI check",
233
- codex: DEEP_UI_CHECK_CODEX_HANDOFF,
234
- description: "JSKIT can ask Codex for a focused UI quality pass; use Next to continue without one.",
235
- id: "deep_ui_check_run",
236
- kind: "codex_prompt",
237
- label: "Run deep UI check",
238
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app"]
239
- }),
240
- defineStep({
241
- buttonLabel: "Run deslop",
242
- codex: REVIEW_EXECUTION_CODEX_HANDOFF,
243
- description: "JSKIT sends the current implementation to Codex for a review/deslop pass; the user decides whether to resolve findings, run deslop again, or continue.",
244
- id: "review_prompt_rendered",
245
- kind: "codex_prompt",
246
- label: "Run deslop",
247
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "deep_ui_check_satisfied"]
248
- }),
249
- defineStep({
250
- buttonLabel: "Accept review/deslop",
251
- description: "User chooses whether to resolve the last deslop result, run deslop again, or continue.",
252
- id: "review_changes_accepted",
253
- kind: "user_check",
254
- label: "Accept review/deslop",
255
- nextCommandTemplate: `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} step --review-findings-remaining false`,
256
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "deep_ui_check_satisfied"],
257
- submitOptions: Object.freeze({
258
- reviewFindingsRemaining: false
259
- })
260
- }),
261
- defineStep({
262
- buttonLabel: "Run automated checks",
263
- codex: AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF,
264
- description: "JSKIT can ask Codex to run the official verification command in the worktree; use Next to continue without one.",
265
- id: "automated_checks_run",
266
- kind: "codex_prompt",
267
- label: "Run automated checks",
268
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "deep_ui_check_satisfied"]
269
- }),
270
- defineStep({
271
- buttonLabel: "Complete user check",
272
- description: "User manually checks the result; if it fails, rewind to the step that should be redone.",
273
- id: "user_check_completed",
274
- input: USER_CHECK_INPUT,
275
- kind: "user_check",
276
- label: "Complete user check",
277
- nextCommandTemplate: `${JSKIT_CLI_SHELL_COMMAND} session {{session_id}} step --user-check passed`,
278
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "automated_checks_passed", "deep_ui_check_satisfied"],
279
- utilityActions: Object.freeze([
280
- Object.freeze({
281
- id: "session_app_test",
282
- kind: "app_test",
283
- label: "Test app"
284
- })
285
- ])
286
- }),
287
- defineStep({
288
- buttonLabel: "Update blueprint",
289
- codex: BLUEPRINT_CODEX_HANDOFF,
290
- description: "JSKIT asks Codex to update durable app memory from the accepted work; Codex edits .jskit/APP_BLUEPRINT.md; JSKIT records the update for the accepted-work commit.",
291
- id: "blueprint_updated",
292
- kind: "codex_prompt",
293
- label: "Update blueprint",
294
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "automated_checks_passed", "deep_ui_check_satisfied", "user_check_passed"]
295
- }),
296
- defineStep({
297
- buttonLabel: "Commit changes",
298
- description: "JSKIT commits the accepted session changes, including durable app memory updates, in the session worktree.",
299
- id: "changes_committed",
300
- label: "Commit changes",
301
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "issue_url_exists", "github_auth", "automated_checks_passed", "deep_ui_check_satisfied", "user_check_passed", "blueprint_update_satisfied"]
302
- }),
303
- defineStep({
304
- buttonLabel: "Create PR file",
305
- codex: PR_FILE_CODEX_HANDOFF,
306
- description: "JSKIT renders the PR-file prompt; Codex writes pull_request.md for review.",
307
- id: "final_report_created",
308
- kind: "codex_prompt",
309
- label: "Create PR file",
310
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "automated_checks_passed", "deep_ui_check_satisfied", "user_check_passed", "blueprint_update_satisfied", "accepted_changes_committed"]
311
- }),
312
- defineStep({
313
- buttonLabel: "Create PR on GH",
314
- description: "JSKIT creates the GitHub pull request from the reviewed pull_request.md and records the PR URL.",
315
- id: "pr_created",
316
- label: "Edit and create PR",
317
- preconditions: ["session_exists", "worktree_exists", "dependencies_installed", "ready_jskit_app", "automated_checks_passed", "deep_ui_check_satisfied", "user_check_passed", "accepted_changes_committed", "blueprint_update_satisfied", "pull_request_file_exists"]
318
- }),
319
- defineStep({
320
- buttonLabel: "Merge",
321
- codex: PR_MERGE_PREP_CODEX_HANDOFF,
322
- description: "Prepare the pull request for merge, merge it, or use Next to continue without merging.",
323
- id: "pr_merge_prepared",
324
- label: "Merge PR",
325
- preconditions: ["session_exists", "worktree_exists"]
326
- }),
327
- defineStep({
328
- buttonLabel: "Sync main checkout",
329
- description: "JSKIT fast-forwards the main checkout after a merged PR. If the PR was not merged, use Next to record that no sync was needed.",
330
- id: "main_checkout_synced",
331
- label: "Sync main checkout",
332
- preconditions: ["session_exists", "worktree_exists"]
333
- }),
334
- defineStep({
335
- buttonLabel: "Finish",
336
- description: "Congratulations! Finish the session by removing the session worktree and archiving the completed session.",
337
- id: "session_finished",
338
- label: "Congratulations!",
339
- preconditions: ["session_exists", "main_checkout_sync_satisfied"]
340
- })
341
- ]);
342
-
343
- const STEP_IDS = Object.freeze(STEP_DEFINITIONS.map((step) => step.id));
344
- const STEP_LABEL_BY_ID = Object.freeze(Object.fromEntries(STEP_DEFINITIONS.map((step) => [step.id, step.label])));
345
- const STEP_DEFINITION_BY_ID = Object.freeze(Object.fromEntries(STEP_DEFINITIONS.map((step) => [step.id, step])));
346
- const CYCLE_STEP_IDS = Object.freeze([]);
347
- const STEP_PRECONDITION_NAMES = Object.freeze(Object.fromEntries(
348
- STEP_DEFINITIONS.map((step) => [step.id, step.preconditions])
349
- ));
350
-
351
- export {
352
- PROMPT_DIRECTORY,
353
- SESSION_ID_PATTERN,
354
- SESSION_STATUS,
355
- SESSION_WORKFLOW_VERSION,
356
- DEPENDENCIES_INSTALL_RESULT_FILE,
357
- REVIEW_PASS_LIMIT,
358
- CYCLE_STEP_IDS,
359
- STEP_DEFINITION_BY_ID,
360
- STEP_DEFINITIONS,
361
- STEP_IDS,
362
- STEP_LABEL_BY_ID,
363
- STEP_PRECONDITION_NAMES,
364
- ISSUE_DEFINITION_CODEX_HANDOFF,
365
- ISSUE_FILE_CODEX_HANDOFF,
366
- PLAN_CODEX_HANDOFF,
367
- PLAN_EXECUTION_CODEX_HANDOFF,
368
- REVIEW_EXECUTION_CODEX_HANDOFF,
369
- RESOLVE_DESLOP_CODEX_HANDOFF,
370
- DEEP_UI_CHECK_CODEX_HANDOFF,
371
- AUTOMATED_CHECK_REPAIR_CODEX_HANDOFF,
372
- BLUEPRINT_CODEX_HANDOFF,
373
- PR_FILE_CODEX_HANDOFF,
374
- PR_MERGE_PREP_CODEX_HANDOFF,
375
- JSKIT_CLI_SHELL_COMMAND,
376
- SESSION_STATE_RELATIVE_PATH
377
- };
@@ -1,97 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { constants as fsConstants } from "node:fs";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
-
7
- const execFileAsync = promisify(execFile);
8
-
9
- function normalizeText(value) {
10
- return String(value || "").trim();
11
- }
12
-
13
- function timestampForStepRecord(now = new Date()) {
14
- return now.toISOString();
15
- }
16
-
17
- async function fileExists(filePath) {
18
- try {
19
- await access(filePath, fsConstants.F_OK);
20
- return true;
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- async function readTextIfExists(filePath) {
27
- if (!filePath || !(await fileExists(filePath))) {
28
- return "";
29
- }
30
- return readFile(filePath, "utf8");
31
- }
32
-
33
- async function readTrimmedFile(filePath) {
34
- return normalizeText(await readTextIfExists(filePath));
35
- }
36
-
37
- async function writeTextFile(filePath, value) {
38
- await mkdir(path.dirname(filePath), { recursive: true });
39
- await writeFile(filePath, `${String(value || "").replace(/\s*$/u, "")}\n`, "utf8");
40
- }
41
-
42
- async function runCommand(command, args = [], { cwd, env = {}, timeout = 30000 } = {}) {
43
- try {
44
- const result = await execFileAsync(command, args, {
45
- cwd,
46
- env: {
47
- ...process.env,
48
- ...env
49
- },
50
- maxBuffer: 1024 * 1024 * 10,
51
- timeout
52
- });
53
- return {
54
- exitCode: 0,
55
- ok: true,
56
- output: String([result.stdout, result.stderr].filter(Boolean).join("\n")).trim(),
57
- stderr: String(result.stderr || "").trim(),
58
- stdout: String(result.stdout || "").trim()
59
- };
60
- } catch (error) {
61
- const stdout = String(error?.stdout || "").trim();
62
- const stderr = String(error?.stderr || "").trim();
63
- return {
64
- exitCode: typeof error?.code === "number" ? error.code : 1,
65
- ok: false,
66
- output: String(error?.message || [stdout, stderr].filter(Boolean).join("\n")).trim(),
67
- stderr,
68
- stdout
69
- };
70
- }
71
- }
72
-
73
- async function runGit(targetRoot, args = [], options = {}) {
74
- return runCommand("git", args, {
75
- cwd: targetRoot,
76
- ...options
77
- });
78
- }
79
-
80
- async function runGitInWorktree(worktree, args = [], options = {}) {
81
- return runCommand("git", args, {
82
- cwd: worktree,
83
- ...options
84
- });
85
- }
86
-
87
- export {
88
- fileExists,
89
- normalizeText,
90
- readTextIfExists,
91
- readTrimmedFile,
92
- runCommand,
93
- runGit,
94
- runGitInWorktree,
95
- timestampForStepRecord,
96
- writeTextFile
97
- };
@@ -1,163 +0,0 @@
1
- import { mkdir, rename } from "node:fs/promises";
2
- import path from "node:path";
3
- import {
4
- SESSION_ID_PATTERN,
5
- SESSION_STATE_RELATIVE_PATH
6
- } from "./constants.js";
7
- import {
8
- fileExists,
9
- normalizeText
10
- } from "./io.js";
11
-
12
- function formatDatePart(value) {
13
- return String(value).padStart(2, "0");
14
- }
15
-
16
- function createSessionId(now = new Date()) {
17
- const year = now.getFullYear();
18
- const month = formatDatePart(now.getMonth() + 1);
19
- const day = formatDatePart(now.getDate());
20
- const hour = formatDatePart(now.getHours());
21
- const minute = formatDatePart(now.getMinutes());
22
- const second = formatDatePart(now.getSeconds());
23
- return `${year}-${month}-${day}_${hour}-${minute}-${second}`;
24
- }
25
-
26
- async function createAvailableSessionId(targetRoot, now = new Date()) {
27
- const baseSessionId = createSessionId(now);
28
- const basePaths = resolveSessionPaths({ targetRoot, sessionId: baseSessionId });
29
- if (
30
- !(await fileExists(basePaths.sessionRoot)) &&
31
- !(await fileExists(basePaths.completedSessionRoot)) &&
32
- !(await fileExists(basePaths.abandonedSessionRoot))
33
- ) {
34
- return baseSessionId;
35
- }
36
-
37
- for (let index = 1; index <= 36 ** 4 - 1; index += 1) {
38
- const suffix = index.toString(36).padStart(4, "0");
39
- const candidate = `${baseSessionId}-${suffix}`;
40
- const candidatePaths = resolveSessionPaths({ targetRoot, sessionId: candidate });
41
- if (
42
- !(await fileExists(candidatePaths.sessionRoot)) &&
43
- !(await fileExists(candidatePaths.completedSessionRoot)) &&
44
- !(await fileExists(candidatePaths.abandonedSessionRoot))
45
- ) {
46
- return candidate;
47
- }
48
- }
49
-
50
- throw new Error(`No available session id found for timestamp ${baseSessionId}.`);
51
- }
52
-
53
- function isValidSessionId(sessionId = "") {
54
- return SESSION_ID_PATTERN.test(normalizeText(sessionId));
55
- }
56
-
57
- function normalizeSessionId(sessionId = "") {
58
- const normalized = normalizeText(sessionId);
59
- if (!isValidSessionId(normalized)) {
60
- throw new Error(`Invalid session id "${sessionId}". Expected YYYY-MM-DD_HH-MM-SS.`);
61
- }
62
- return normalized;
63
- }
64
-
65
- function resolveSessionPaths({ targetRoot, sessionId = "" } = {}) {
66
- const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
67
- const sessionStateRoot = path.join(normalizedTargetRoot, SESSION_STATE_RELATIVE_PATH);
68
- const sessionsRoot = path.join(sessionStateRoot, "active");
69
- const completedSessionsRoot = path.join(sessionStateRoot, "completed");
70
- const abandonedSessionsRoot = path.join(sessionStateRoot, "abandoned");
71
- const normalizedSessionId = sessionId ? normalizeSessionId(sessionId) : "";
72
- const sessionRoot = normalizedSessionId ? path.join(sessionsRoot, normalizedSessionId) : "";
73
- const worktree = normalizedSessionId ? path.join(sessionRoot, "worktree") : "";
74
- const branch = normalizedSessionId ? `jskit-studio/${normalizedSessionId}` : "";
75
-
76
- return Object.freeze({
77
- abandonedSessionRoot: normalizedSessionId ? path.join(abandonedSessionsRoot, normalizedSessionId) : "",
78
- abandonedSessionsRoot,
79
- branch,
80
- completedSessionRoot: normalizedSessionId ? path.join(completedSessionsRoot, normalizedSessionId) : "",
81
- completedSessionsRoot,
82
- sessionId: normalizedSessionId,
83
- sessionRoot,
84
- sessionsRoot,
85
- sessionStateRoot,
86
- targetRoot: normalizedTargetRoot,
87
- worktree
88
- });
89
- }
90
-
91
- async function resolveExistingSessionRoot(paths) {
92
- if (paths.archive && await fileExists(paths.sessionRoot)) {
93
- return {
94
- archive: paths.archive,
95
- root: paths.sessionRoot
96
- };
97
- }
98
- if (await fileExists(paths.sessionRoot)) {
99
- return {
100
- archive: "active",
101
- root: paths.sessionRoot
102
- };
103
- }
104
- if (await fileExists(paths.completedSessionRoot)) {
105
- return {
106
- archive: "completed",
107
- root: paths.completedSessionRoot
108
- };
109
- }
110
- if (await fileExists(paths.abandonedSessionRoot)) {
111
- return {
112
- archive: "abandoned",
113
- root: paths.abandonedSessionRoot
114
- };
115
- }
116
- return {
117
- archive: "",
118
- root: ""
119
- };
120
- }
121
-
122
- async function pathsForExistingSession(paths) {
123
- const existing = await resolveExistingSessionRoot(paths);
124
- if (!existing.root || existing.root === paths.sessionRoot) {
125
- return paths;
126
- }
127
- return Object.freeze({
128
- ...paths,
129
- archive: existing.archive,
130
- sessionRoot: existing.root
131
- });
132
- }
133
-
134
- async function archiveSession(paths, archive) {
135
- const archiveRoot = archive === "completed" ? paths.completedSessionRoot : paths.abandonedSessionRoot;
136
- if (!archiveRoot || paths.sessionRoot === archiveRoot) {
137
- return paths;
138
- }
139
- if (!(await fileExists(paths.sessionRoot))) {
140
- return pathsForExistingSession(paths);
141
- }
142
- if (await fileExists(archiveRoot)) {
143
- throw new Error(`Cannot archive session ${paths.sessionId}; target already exists: ${archiveRoot}`);
144
- }
145
- await mkdir(path.dirname(archiveRoot), { recursive: true });
146
- await rename(paths.sessionRoot, archiveRoot);
147
- return Object.freeze({
148
- ...paths,
149
- archive,
150
- sessionRoot: archiveRoot
151
- });
152
- }
153
-
154
- export {
155
- archiveSession,
156
- createAvailableSessionId,
157
- createSessionId,
158
- isValidSessionId,
159
- normalizeSessionId,
160
- resolveExistingSessionRoot,
161
- resolveSessionPaths,
162
- pathsForExistingSession
163
- };