@vibecompass/vibecompass 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -13
- package/package.json +1 -1
- package/src/cli.js +161 -2
- package/src/docs-review.js +813 -0
- package/src/generators/agent-files/template.js +3 -3
- package/src/index.js +1 -0
- package/src/init.js +3 -1
- package/src/manifest.js +34 -1
- package/src/project-memory.js +1 -1
- package/src/scaffold.js +220 -14
- package/src/session.js +256 -25
- package/src/setup.js +54 -0
package/src/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { scanProjectMemory } from './project-memory.js';
|
|
3
4
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
4
5
|
import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
|
|
5
6
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
@@ -36,6 +37,18 @@ export async function startProjectSession(options) {
|
|
|
36
37
|
const claude = await readClaudeFile(normalized.claudePath);
|
|
37
38
|
const sessionId = await resolveStartSessionId(normalized, options);
|
|
38
39
|
const lanePaths = getLanePaths(normalized, sessionId);
|
|
40
|
+
const features = normalizeStringArray(options?.features);
|
|
41
|
+
const repos = normalizeStringArray(options?.repos);
|
|
42
|
+
const claims = normalizeStringArray(options?.claims);
|
|
43
|
+
const existingLanes = await listActiveSessionLanes(normalized);
|
|
44
|
+
const overlapWarnings = buildOverlapWarnings({
|
|
45
|
+
sessionId,
|
|
46
|
+
features,
|
|
47
|
+
repos,
|
|
48
|
+
claims,
|
|
49
|
+
existingLanes,
|
|
50
|
+
});
|
|
51
|
+
const highestDecisionId = await readHighestDecisionId(normalized.rootDir);
|
|
39
52
|
|
|
40
53
|
const nextSessionNumber = await getNextSessionNumber(normalized.sessionsDir, sessionDate);
|
|
41
54
|
const currentSession = parseCurrentSessionBlock(claude.content);
|
|
@@ -68,9 +81,10 @@ export async function startProjectSession(options) {
|
|
|
68
81
|
sessionDate,
|
|
69
82
|
sessionNumber: nextSessionNumber,
|
|
70
83
|
workingOn,
|
|
71
|
-
features
|
|
72
|
-
repos
|
|
73
|
-
claims
|
|
84
|
+
features,
|
|
85
|
+
repos,
|
|
86
|
+
claims,
|
|
87
|
+
highestDecisionId,
|
|
74
88
|
}),
|
|
75
89
|
'utf8',
|
|
76
90
|
);
|
|
@@ -99,6 +113,12 @@ export async function startProjectSession(options) {
|
|
|
99
113
|
status: 'active',
|
|
100
114
|
workingOn,
|
|
101
115
|
}, { current: sessionId });
|
|
116
|
+
const auditWarnings = await buildDocsReviewWarnings(normalized);
|
|
117
|
+
const metadataWarnings = collectLaneMetadataWarnings(existingLanes);
|
|
118
|
+
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
119
|
+
rootDir: normalized.rootDir,
|
|
120
|
+
toolingRootDir: normalized.toolingRootDir,
|
|
121
|
+
});
|
|
102
122
|
|
|
103
123
|
return {
|
|
104
124
|
rootDir: normalized.rootDir,
|
|
@@ -110,6 +130,12 @@ export async function startProjectSession(options) {
|
|
|
110
130
|
handoffFilePath: lanePaths.handoffFilePath,
|
|
111
131
|
sessionDate,
|
|
112
132
|
sessionNumber: nextSessionNumber,
|
|
133
|
+
warnings: [
|
|
134
|
+
...metadataWarnings,
|
|
135
|
+
...overlapWarnings,
|
|
136
|
+
...auditWarnings,
|
|
137
|
+
],
|
|
138
|
+
agentFileSync,
|
|
113
139
|
};
|
|
114
140
|
}
|
|
115
141
|
|
|
@@ -185,9 +211,10 @@ export async function closeProjectSession(options) {
|
|
|
185
211
|
);
|
|
186
212
|
|
|
187
213
|
const hadHandoff = await fileExists(lanePaths.handoffFilePath);
|
|
214
|
+
let activeSessionIndex = null;
|
|
188
215
|
if (sessionId) {
|
|
189
216
|
await rm(lanePaths.laneDir, { recursive: true, force: true });
|
|
190
|
-
await removeActiveSessionFromIndex(normalized, sessionId);
|
|
217
|
+
activeSessionIndex = await removeActiveSessionFromIndex(normalized, sessionId);
|
|
191
218
|
} else {
|
|
192
219
|
await rm(lanePaths.wipFilePath, { force: true });
|
|
193
220
|
if (hadHandoff) {
|
|
@@ -195,16 +222,22 @@ export async function closeProjectSession(options) {
|
|
|
195
222
|
}
|
|
196
223
|
}
|
|
197
224
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
const closedSummary = `Closed session ${activeSession.sessionNumber} and wrote \`${sessionRelativePath}\`.`;
|
|
226
|
+
const updatedClaude = replaceCurrentSessionBlock(
|
|
227
|
+
claude.content,
|
|
228
|
+
activeSessionIndex?.current
|
|
229
|
+
? buildCurrentSessionFieldsForLane(activeSessionIndex.lanes.find((lane) => lane.id === activeSessionIndex.current), {
|
|
230
|
+
lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
|
|
231
|
+
})
|
|
232
|
+
: {
|
|
233
|
+
date: `${activeSession.sessionDate} (session ${activeSession.sessionNumber})`,
|
|
234
|
+
workingOn: 'Session closed. Ready for the next builder session.',
|
|
235
|
+
lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
|
|
236
|
+
blockers: blockers.length > 0 ? summarizeList(blockers) : 'No blocker remains.',
|
|
237
|
+
nextSessionShould:
|
|
238
|
+
normalizeOptionalString(options?.nextSessionShould) ?? summarizeOrderedList(nextSteps),
|
|
239
|
+
},
|
|
240
|
+
);
|
|
208
241
|
|
|
209
242
|
await writeFile(normalized.claudePath, updatedClaude, 'utf8');
|
|
210
243
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
@@ -248,17 +281,35 @@ export async function switchProjectSession(options = {}) {
|
|
|
248
281
|
const normalized = normalizeSessionPaths(options);
|
|
249
282
|
const sessionId = validateLaneId(requireNonEmptyString(options?.sessionId, 'switch-session requires a session ID.'));
|
|
250
283
|
const lanes = await listActiveSessionLanes(normalized);
|
|
284
|
+
const lane = lanes.find((item) => item.id === sessionId);
|
|
251
285
|
|
|
252
|
-
if (!
|
|
286
|
+
if (!lane) {
|
|
253
287
|
throw new Error(`Active session lane "${sessionId}" does not exist.`);
|
|
254
288
|
}
|
|
255
289
|
|
|
256
290
|
await upsertActiveSessionIndex(normalized, null, { current: sessionId });
|
|
291
|
+
const claude = await readClaudeFile(normalized.claudePath);
|
|
292
|
+
await writeFile(
|
|
293
|
+
normalized.claudePath,
|
|
294
|
+
replaceCurrentSessionBlock(
|
|
295
|
+
claude.content,
|
|
296
|
+
buildCurrentSessionFieldsForLane(lane, {
|
|
297
|
+
lastThingCompleted: `Switched current lane to ${sessionId}.`,
|
|
298
|
+
}),
|
|
299
|
+
),
|
|
300
|
+
'utf8',
|
|
301
|
+
);
|
|
302
|
+
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
303
|
+
rootDir: normalized.rootDir,
|
|
304
|
+
toolingRootDir: normalized.toolingRootDir,
|
|
305
|
+
});
|
|
257
306
|
|
|
258
307
|
return {
|
|
259
308
|
rootDir: normalized.rootDir,
|
|
309
|
+
claudePath: normalized.claudePath,
|
|
260
310
|
current: sessionId,
|
|
261
311
|
lanes,
|
|
312
|
+
agentFileSync,
|
|
262
313
|
};
|
|
263
314
|
}
|
|
264
315
|
|
|
@@ -284,6 +335,26 @@ async function syncAgentInstructionFilesSafely(options) {
|
|
|
284
335
|
}
|
|
285
336
|
}
|
|
286
337
|
|
|
338
|
+
async function buildDocsReviewWarnings(normalized) {
|
|
339
|
+
const markerPath = path.join(normalized.rootDir, 'state', 'docs-review.json');
|
|
340
|
+
try {
|
|
341
|
+
const marker = JSON.parse(await readFile(markerPath, 'utf8'));
|
|
342
|
+
if (marker && marker.status === 'completed') {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')) {
|
|
347
|
+
return [
|
|
348
|
+
`Docs-review marker at ${markerPath} is unreadable. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
`No docs-review marker found at ${markerPath}. Starter docs are not a comprehensive architecture review; run "vibecompass docs-review --guided" before risky implementation work.`,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
|
|
287
358
|
function normalizeSessionPaths(options) {
|
|
288
359
|
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
289
360
|
const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
|
|
@@ -397,6 +468,26 @@ function replaceCurrentSessionBlock(content, fields) {
|
|
|
397
468
|
return `${content.slice(0, sessionFence.bodyStart)}${blockBody}${content.slice(sessionFence.bodyEnd)}`;
|
|
398
469
|
}
|
|
399
470
|
|
|
471
|
+
function buildCurrentSessionFieldsForLane(lane, overrides = {}) {
|
|
472
|
+
if (!lane) {
|
|
473
|
+
throw new Error('Cannot update the Current session block without active lane metadata.');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const sessionDate = lane.sessionDate ?? normalizeSessionDate();
|
|
477
|
+
const sessionNumber = lane.sessionNumber || 0;
|
|
478
|
+
const workingOn = lane.workingOn ?? 'No summary recorded.';
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
date: `${sessionDate} (session ${sessionNumber}, lane ${lane.id})`,
|
|
482
|
+
workingOn: `${workingOn} [${lane.id}]`,
|
|
483
|
+
lastThingCompleted:
|
|
484
|
+
overrides.lastThingCompleted ?? 'Current lane selected from `sessions/active/index.yaml`.',
|
|
485
|
+
blockers: overrides.blockers ?? 'No blocker recorded for the selected lane.',
|
|
486
|
+
nextSessionShould:
|
|
487
|
+
overrides.nextSessionShould ?? 'Continue the selected active lane from `sessions/active/index.yaml`.',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
400
491
|
async function getNextSessionNumber(sessionsDir, sessionDate) {
|
|
401
492
|
const sessions = await listFinalizedSessions(sessionsDir);
|
|
402
493
|
const activeSessions = await listActiveSessionLanes({ sessionsDir, activeSessionsDir: path.join(sessionsDir, 'active') });
|
|
@@ -414,18 +505,13 @@ async function resolveStartSessionId(normalized, options) {
|
|
|
414
505
|
return validateLaneId(explicitId);
|
|
415
506
|
}
|
|
416
507
|
|
|
417
|
-
const lanes = await listActiveSessionLanes(normalized);
|
|
418
|
-
if (lanes.length > 0) {
|
|
419
|
-
throw new Error('Active session lanes already exist. Pass --id to start another lane explicitly.');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
508
|
if ((await fileExists(normalized.wipFilePath)) || (await fileExists(normalized.handoffFilePath))) {
|
|
423
509
|
throw new Error(
|
|
424
510
|
`Legacy active session scratch files already exist under ${normalized.sessionsDir}. Close or recover that session before starting a lane.`,
|
|
425
511
|
);
|
|
426
512
|
}
|
|
427
513
|
|
|
428
|
-
|
|
514
|
+
throw new Error('start-session requires --id <lane-id> so each active lane has a meaningful name.');
|
|
429
515
|
}
|
|
430
516
|
|
|
431
517
|
async function resolveExistingSessionId(normalized, options) {
|
|
@@ -466,6 +552,9 @@ async function listActiveSessionLanes(normalized) {
|
|
|
466
552
|
features: metadata.features,
|
|
467
553
|
repos: metadata.repos,
|
|
468
554
|
claims: metadata.claims,
|
|
555
|
+
startedAt: metadata.startedAt,
|
|
556
|
+
decisionSnapshot: metadata.decisionSnapshot,
|
|
557
|
+
warnings: metadata.warnings,
|
|
469
558
|
sessionDate: metadata.sessionDate ?? wipSession?.sessionDate ?? null,
|
|
470
559
|
sessionNumber: metadata.sessionNumber ?? wipSession?.sessionNumber ?? 0,
|
|
471
560
|
});
|
|
@@ -499,18 +588,30 @@ async function readLaneMetadata(sessionFilePath) {
|
|
|
499
588
|
features: normalizeStringArray(data.feature_slugs),
|
|
500
589
|
repos: normalizeStringArray(data.repos),
|
|
501
590
|
claims: normalizeStringArray(data.claimed_paths),
|
|
591
|
+
startedAt: normalizeOptionalString(data.started_at),
|
|
592
|
+
decisionSnapshot: {
|
|
593
|
+
highestDecisionId: parseNullableNumber(data.decision_snapshot?.highest_decision_id),
|
|
594
|
+
},
|
|
502
595
|
sessionDate: normalizeOptionalString(data.session_date),
|
|
503
596
|
sessionNumber: typeof data.session_number === 'number' ? data.session_number : Number(data.session_number) || null,
|
|
597
|
+
warnings: [],
|
|
504
598
|
};
|
|
505
|
-
} catch {
|
|
599
|
+
} catch (error) {
|
|
506
600
|
return {
|
|
507
601
|
status: null,
|
|
508
602
|
workingOn: null,
|
|
509
603
|
features: [],
|
|
510
604
|
repos: [],
|
|
511
605
|
claims: [],
|
|
606
|
+
startedAt: null,
|
|
607
|
+
decisionSnapshot: {
|
|
608
|
+
highestDecisionId: null,
|
|
609
|
+
},
|
|
512
610
|
sessionDate: null,
|
|
513
611
|
sessionNumber: null,
|
|
612
|
+
warnings: [
|
|
613
|
+
`Could not parse ${sessionFilePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
614
|
+
],
|
|
514
615
|
};
|
|
515
616
|
}
|
|
516
617
|
}
|
|
@@ -547,16 +648,28 @@ async function upsertActiveSessionIndex(normalized, lane, options = {}) {
|
|
|
547
648
|
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, [...laneMap.values()]), 'utf8');
|
|
548
649
|
}
|
|
549
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Removes a closed lane from the active index and returns the surviving lane
|
|
653
|
+
* state used to refresh the Current session block. `lanes` contains the full
|
|
654
|
+
* lane metadata read from each sibling lane's `session.yaml` / `wip.md`.
|
|
655
|
+
*/
|
|
550
656
|
async function removeActiveSessionFromIndex(normalized, sessionId) {
|
|
551
657
|
const index = await readActiveSessionIndex(normalized);
|
|
552
658
|
const lanes = (await listActiveSessionLanes(normalized)).filter((lane) => lane.id !== sessionId);
|
|
553
659
|
const current = index.current === sessionId ? lanes[0]?.id ?? null : index.current;
|
|
554
660
|
if (lanes.length === 0) {
|
|
555
661
|
await rm(normalized.activeSessionsIndexPath, { force: true });
|
|
556
|
-
return
|
|
662
|
+
return {
|
|
663
|
+
current: null,
|
|
664
|
+
lanes,
|
|
665
|
+
};
|
|
557
666
|
}
|
|
558
667
|
|
|
559
668
|
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, lanes), 'utf8');
|
|
669
|
+
return {
|
|
670
|
+
current,
|
|
671
|
+
lanes,
|
|
672
|
+
};
|
|
560
673
|
}
|
|
561
674
|
|
|
562
675
|
async function listFinalizedSessions(sessionsDir) {
|
|
@@ -721,9 +834,9 @@ function renderLaneMetadata(options) {
|
|
|
721
834
|
renderYamlArray('feature_slugs', options.features),
|
|
722
835
|
renderYamlArray('repos', options.repos),
|
|
723
836
|
renderYamlArray('claimed_paths', options.claims),
|
|
724
|
-
`started_at: ${new Date()
|
|
837
|
+
`started_at: ${formatLocalDateTime(new Date())}`,
|
|
725
838
|
'decision_snapshot:',
|
|
726
|
-
|
|
839
|
+
` highest_decision_id: ${options.highestDecisionId ?? 'null'}`,
|
|
727
840
|
'',
|
|
728
841
|
].join('\n');
|
|
729
842
|
}
|
|
@@ -835,6 +948,124 @@ function normalizeSessionDate(value) {
|
|
|
835
948
|
].join('-');
|
|
836
949
|
}
|
|
837
950
|
|
|
951
|
+
async function readHighestDecisionId(rootDir) {
|
|
952
|
+
try {
|
|
953
|
+
const scanResult = await scanProjectMemory(rootDir);
|
|
954
|
+
const decisionIds = scanResult.documents.flatMap((document) => document.extracted?.decision_ids ?? []);
|
|
955
|
+
return decisionIds.reduce((max, id) => Math.max(max, id), 0) || null;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function buildOverlapWarnings(options) {
|
|
962
|
+
const warnings = [];
|
|
963
|
+
for (const lane of options.existingLanes) {
|
|
964
|
+
const sharedFeatures = intersect(options.features, lane.features ?? []);
|
|
965
|
+
if (sharedFeatures.length > 0) {
|
|
966
|
+
warnings.push(
|
|
967
|
+
`Active session lane "${options.sessionId}" overlaps "${lane.id}" on feature(s): ${sharedFeatures.join(', ')}.`,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const claimOverlaps = findClaimOverlaps(options.claims, lane.claims ?? []);
|
|
972
|
+
for (const overlap of claimOverlaps) {
|
|
973
|
+
warnings.push(
|
|
974
|
+
`Active session lane "${options.sessionId}" overlaps "${lane.id}" on claimed path ${overlap.repo}:${overlap.path}.`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const sharedRepos = intersect(options.repos, lane.repos ?? []);
|
|
979
|
+
if (sharedRepos.length > 0 && options.claims.length === 0 && (lane.claims ?? []).length === 0) {
|
|
980
|
+
warnings.push(
|
|
981
|
+
`Active session lane "${options.sessionId}" shares repo(s) ${sharedRepos.join(', ')} with "${lane.id}" without path claims; add --claim values to clarify ownership.`,
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return warnings;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function collectLaneMetadataWarnings(lanes) {
|
|
990
|
+
return lanes.flatMap((lane) => lane.warnings ?? []);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function intersect(left, right) {
|
|
994
|
+
const rightSet = new Set(right);
|
|
995
|
+
return left.filter((item) => rightSet.has(item));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function findClaimOverlaps(leftClaims, rightClaims) {
|
|
999
|
+
const overlaps = [];
|
|
1000
|
+
for (const left of leftClaims.map(parseClaim).filter(Boolean)) {
|
|
1001
|
+
for (const right of rightClaims.map(parseClaim).filter(Boolean)) {
|
|
1002
|
+
if (left.repo !== right.repo) {
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (pathPrefixesOverlap(left.path, right.path)) {
|
|
1007
|
+
overlaps.push({
|
|
1008
|
+
repo: left.repo,
|
|
1009
|
+
path: left.path.length <= right.path.length ? left.path : right.path,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return overlaps;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function parseClaim(value) {
|
|
1019
|
+
const separatorIndex = value.indexOf(':');
|
|
1020
|
+
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
repo: value.slice(0, separatorIndex),
|
|
1026
|
+
path: normalizeClaimPath(value.slice(separatorIndex + 1)),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function normalizeClaimPath(value) {
|
|
1031
|
+
return value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function pathPrefixesOverlap(left, right) {
|
|
1035
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function parseNullableNumber(value) {
|
|
1039
|
+
if (value === null || value === undefined) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const number = typeof value === 'number' ? value : Number(value);
|
|
1044
|
+
return Number.isFinite(number) ? number : null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function formatLocalDateTime(date) {
|
|
1048
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
1049
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
1050
|
+
const absOffset = Math.abs(offsetMinutes);
|
|
1051
|
+
const offset = `${sign}${String(Math.floor(absOffset / 60)).padStart(2, '0')}:${String(absOffset % 60).padStart(2, '0')}`;
|
|
1052
|
+
|
|
1053
|
+
return [
|
|
1054
|
+
date.getFullYear(),
|
|
1055
|
+
'-',
|
|
1056
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
1057
|
+
'-',
|
|
1058
|
+
String(date.getDate()).padStart(2, '0'),
|
|
1059
|
+
'T',
|
|
1060
|
+
String(date.getHours()).padStart(2, '0'),
|
|
1061
|
+
':',
|
|
1062
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
1063
|
+
':',
|
|
1064
|
+
String(date.getSeconds()).padStart(2, '0'),
|
|
1065
|
+
offset,
|
|
1066
|
+
].join('');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
838
1069
|
function requireNonEmptyString(value, message) {
|
|
839
1070
|
const normalized = normalizeOptionalString(value);
|
|
840
1071
|
if (!normalized) {
|
package/src/setup.js
CHANGED
|
@@ -40,6 +40,10 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
40
40
|
throw new Error('init requires --session-working-on when --start-session is used without guided setup.');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
if (resolved.startSession && !resolved.sessionId) {
|
|
44
|
+
throw new Error('init requires --session-id when --start-session is used.');
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
return {
|
|
44
48
|
initOptions: {
|
|
45
49
|
rootDir: resolved.rootDir,
|
|
@@ -62,6 +66,7 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
62
66
|
guidedSummary,
|
|
63
67
|
sessionPlan: resolved.startSession
|
|
64
68
|
? {
|
|
69
|
+
sessionId: resolved.sessionId,
|
|
65
70
|
workingOn: resolved.sessionWorkingOn,
|
|
66
71
|
}
|
|
67
72
|
: null,
|
|
@@ -278,6 +283,15 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
278
283
|
if (!resolved.sessionWorkingOn) {
|
|
279
284
|
resolved.sessionWorkingOn = await askInput(prompter, 'What are you working on?');
|
|
280
285
|
}
|
|
286
|
+
|
|
287
|
+
if (!resolved.sessionId) {
|
|
288
|
+
resolved.sessionId = await askInput(prompter, 'Session lane id', {
|
|
289
|
+
defaultValue: suggestLaneId(resolved.sessionWorkingOn),
|
|
290
|
+
validate(value) {
|
|
291
|
+
return validateLaneIdForPrompt(value);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
if (resolved.closeSessionGitPublish === undefined && (resolved.bootstrap.workflow || resolved.startSession)) {
|
|
@@ -669,6 +683,46 @@ function suggestRepoId(value) {
|
|
|
669
683
|
return slug || 'repo';
|
|
670
684
|
}
|
|
671
685
|
|
|
686
|
+
function suggestLaneId(value) {
|
|
687
|
+
const slug = String(value ?? '')
|
|
688
|
+
.toLowerCase()
|
|
689
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
690
|
+
.replace(/^-+|-+$/g, '')
|
|
691
|
+
.slice(0, 63)
|
|
692
|
+
.replace(/-+$/g, '');
|
|
693
|
+
|
|
694
|
+
return slug.length >= 3 ? slug : 'builder-session';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function validateLaneIdForPrompt(value) {
|
|
698
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(value)) {
|
|
699
|
+
return 'Lane id must be 3-64 lowercase letters, numbers, or hyphens, and must start and end with a letter or number.';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if ([
|
|
703
|
+
'active',
|
|
704
|
+
'current',
|
|
705
|
+
'default',
|
|
706
|
+
'false',
|
|
707
|
+
'handoff',
|
|
708
|
+
'index',
|
|
709
|
+
'new',
|
|
710
|
+
'no',
|
|
711
|
+
'null',
|
|
712
|
+
'off',
|
|
713
|
+
'on',
|
|
714
|
+
'sessions',
|
|
715
|
+
'state',
|
|
716
|
+
'true',
|
|
717
|
+
'wip',
|
|
718
|
+
'yes',
|
|
719
|
+
].includes(value)) {
|
|
720
|
+
return `Lane id "${value}" is reserved.`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
672
726
|
function getRepoDirectoryHints(repo) {
|
|
673
727
|
const hints = new Set();
|
|
674
728
|
|