@straiffi/archon 1.2.4 → 1.2.6

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.
@@ -16,6 +16,7 @@ import { createCorsOriginResolver, parseCorsOrigins } from './lib/cors.js';
16
16
  import { branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorktree, getBundleReviewDiffForCwd, getGitDiffData, getGitDiffFiles, getGitStatus, getGitUnifiedDiff, getProjectBranch, getProjectBranchSuggestions, getSelectedGitUnifiedDiff, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, resolveExistingWorktreePath, resolveWorktreePath, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd } from './lib/git.js';
17
17
  import config from './lib/config.js';
18
18
  import { autoConfigProject, ProjectAutoConfigError } from './lib/projectAutoConfig.js';
19
+ import { analyzeRepoPath, createInitialCommit, ensureInitialCommitForWorktrees, initializeGitRepo, isInitialCommitRequiredError, } from './lib/projectRepos.js';
19
20
  import { browseRepoPath } from './lib/directoryPicker.js';
20
21
  import { resolveDesktopServerHost } from './lib/desktopServerHost.js';
21
22
  import { emitMobileRealtime, getMobileAccessStatus, regenerateMobilePairingChallenge, revokeMobileSession, startMobileAccess, stopMobileAccess, } from './lib/mobileAccess.js';
@@ -43,7 +44,7 @@ import { createGitHubPullRequest, findOpenGitHubPullRequest, findOpenGitHubPullR
43
44
  import { importJiraIssue, JiraApiError, validateJiraConnection } from './lib/integrations/jira.js';
44
45
  import { JiraPlanningReferenceError, resolveJiraPlanningDescription } from './lib/integrations/planning.js';
45
46
  import { getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, upsertBundlePullRequest, } from './lib/bundlePullRequests.js';
46
- import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, validateRepoPath, } from './lib/projects.js';
47
+ import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
47
48
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
48
49
  import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
49
50
  import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
@@ -122,6 +123,7 @@ const prepareBundleWorkspace = async (bundle, project, options = {}) => {
122
123
  const workspacePath = resolveWorktreePath(branch, project);
123
124
  let setupCompleted = false;
124
125
  try {
126
+ ensureInitialCommitForWorktrees(project.repo_path);
125
127
  await createWorktreeAsync(branch, project, { baseBranch: options.baseBranch ?? null });
126
128
  await syncWorktreeFilesAsync(branch, project);
127
129
  await runSetupCommandsInWorkspace(project, workspacePath);
@@ -1435,6 +1437,88 @@ const reviewBundleTickets = (projectId, bundleId) => {
1435
1437
  }),
1436
1438
  };
1437
1439
  };
1440
+ const buildBundleTickets = (projectId, bundleId, broadcaster) => {
1441
+ const project = getProjectById(projectId);
1442
+ if (!project) {
1443
+ return { ok: false, status: 404, error: 'Project not found' };
1444
+ }
1445
+ const bundle = getBundle(bundleId, project.id);
1446
+ if (!bundle) {
1447
+ return { ok: false, status: 404, error: 'Bundle not found' };
1448
+ }
1449
+ const planTickets = listBundleSliceTickets(project.id, bundle.id, 'plan');
1450
+ if (planTickets.length === 0) {
1451
+ return { ok: false, status: 409, error: 'No plan tickets are available to build in this bundle' };
1452
+ }
1453
+ const bundleTicketIds = listBundleTickets(project.id, bundle.id).map(ticket => ticket.id);
1454
+ const orderedPlanTicketIds = getDependencyOrderedBundleTicketIds(project.id, planTickets.map(ticket => ticket.id));
1455
+ const planTicketById = new Map(planTickets.map(ticket => [ticket.id, ticket]));
1456
+ const orderedPlanTickets = orderedPlanTicketIds
1457
+ .map(ticketId => planTicketById.get(ticketId))
1458
+ .filter((ticket) => ticket !== undefined);
1459
+ const bundleBuildBlocker = orderedPlanTickets.find(ticket => getBundleBuildBlockerResponse(ticket) !== null);
1460
+ if (bundleBuildBlocker) {
1461
+ const blockerResponse = getBundleBuildBlockerResponse(bundleBuildBlocker);
1462
+ if (!blockerResponse) {
1463
+ return { ok: false, status: 409, error: 'Unable to build the bundle right now.' };
1464
+ }
1465
+ return { ok: false, status: 409, error: blockerResponse.error, details: blockerResponse };
1466
+ }
1467
+ for (const planTicket of orderedPlanTickets) {
1468
+ const buildValidation = prepareTicketForBuild(planTicket.id, {
1469
+ ignoredBlockerTicketIds: bundleTicketIds,
1470
+ updateState: false,
1471
+ });
1472
+ if (!buildValidation.ok) {
1473
+ return {
1474
+ ok: false,
1475
+ status: buildValidation.status,
1476
+ error: buildValidation.error,
1477
+ ...(buildValidation.blockers ? { details: { blockers: buildValidation.blockers } } : {}),
1478
+ };
1479
+ }
1480
+ }
1481
+ const movedTickets = [];
1482
+ for (const planTicket of orderedPlanTickets) {
1483
+ const buildTransition = prepareTicketForBuild(planTicket.id, {
1484
+ ignoredBlockerTicketIds: bundleTicketIds,
1485
+ });
1486
+ if (!buildTransition.ok) {
1487
+ return {
1488
+ ok: false,
1489
+ status: buildTransition.status,
1490
+ error: buildTransition.error,
1491
+ ...(buildTransition.blockers ? { details: { blockers: buildTransition.blockers } } : {}),
1492
+ };
1493
+ }
1494
+ movedTickets.push(buildTransition.ticket);
1495
+ }
1496
+ emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
1497
+ const details = serializeBundleDetails(project, bundle, {
1498
+ target_ticket_id: movedTickets[0]?.id ?? null,
1499
+ });
1500
+ return {
1501
+ ok: true,
1502
+ status: 200,
1503
+ value: details,
1504
+ afterResponse: () => {
1505
+ for (const [index, movedTicket] of movedTickets.entries()) {
1506
+ if (!movedTicket.project_id || !movedTicket.worktree_bundle_id) {
1507
+ continue;
1508
+ }
1509
+ const queuedBuild = enqueueBundledBuild({
1510
+ ticketId: movedTicket.id,
1511
+ projectId: movedTicket.project_id,
1512
+ worktreeBundleId: movedTicket.worktree_bundle_id,
1513
+ activate: index === 0 && shouldStartBundleBuildNow(movedTicket),
1514
+ });
1515
+ if (queuedBuild.shouldStartNow) {
1516
+ startBuild(movedTicket, broadcaster);
1517
+ }
1518
+ }
1519
+ },
1520
+ };
1521
+ };
1438
1522
  const getCurrentReviewWorkspaceSignature = (ticket) => {
1439
1523
  if (!ticket) {
1440
1524
  return null;
@@ -1951,6 +2035,7 @@ app.post('/mobile-access/enable', async (req, res) => {
1951
2035
  clientBuild: resolveClientBuildPaths(import.meta.url),
1952
2036
  broadcaster: io,
1953
2037
  preferredProjectId,
2038
+ buildBundle: buildBundleTickets,
1954
2039
  reviewBundleTickets,
1955
2040
  getBundleDetails: getProjectBundleDetails,
1956
2041
  submitBundleFollowUp: submitBundleFollowUpResponse,
@@ -2704,7 +2789,7 @@ app.post('/bundles', async (req, res) => {
2704
2789
  }
2705
2790
  catch (error) {
2706
2791
  deleteBundle(bundle.id, result.project.id);
2707
- return res.status(500).json({
2792
+ return res.status(isInitialCommitRequiredError(error) ? 409 : 500).json({
2708
2793
  error: `Unable to prepare the bundle worktree for branch "${bundle.branch}": ${getErrorMessage(error)}`,
2709
2794
  });
2710
2795
  }
@@ -2754,67 +2839,15 @@ app.post('/bundles/:id/build', (req, res) => {
2754
2839
  if ('error' in result) {
2755
2840
  return res.status(400).json({ error: result.error });
2756
2841
  }
2757
- const bundle = getBundle(req.params.id, result.project.id);
2758
- if (!bundle) {
2759
- return res.status(404).json({ error: 'Bundle not found' });
2760
- }
2761
- const planTickets = listBundleSliceTickets(result.project.id, bundle.id, 'plan');
2762
- if (planTickets.length === 0) {
2763
- return res.status(409).json({ error: 'No plan tickets are available to build in this bundle' });
2764
- }
2765
- const bundleTicketIds = listBundleTickets(result.project.id, bundle.id).map(ticket => ticket.id);
2766
- const orderedPlanTicketIds = getDependencyOrderedBundleTicketIds(result.project.id, planTickets.map(ticket => ticket.id));
2767
- const planTicketById = new Map(planTickets.map(ticket => [ticket.id, ticket]));
2768
- const orderedPlanTickets = orderedPlanTicketIds
2769
- .map(ticketId => planTicketById.get(ticketId))
2770
- .filter((ticket) => ticket !== undefined);
2771
- const bundleBuildBlocker = orderedPlanTickets.find(ticket => getBundleBuildBlockerResponse(ticket) !== null);
2772
- if (bundleBuildBlocker) {
2773
- return res.status(409).json(getBundleBuildBlockerResponse(bundleBuildBlocker));
2774
- }
2775
- for (const planTicket of orderedPlanTickets) {
2776
- const buildValidation = prepareTicketForBuild(planTicket.id, {
2777
- ignoredBlockerTicketIds: bundleTicketIds,
2778
- updateState: false,
2779
- });
2780
- if (!buildValidation.ok) {
2781
- return res.status(buildValidation.status).json({
2782
- error: buildValidation.error,
2783
- ...(buildValidation.blockers ? { blockers: buildValidation.blockers } : {}),
2784
- });
2785
- }
2786
- }
2787
- const movedTickets = [];
2788
- for (const planTicket of orderedPlanTickets) {
2789
- const buildTransition = prepareTicketForBuild(planTicket.id, {
2790
- ignoredBlockerTicketIds: bundleTicketIds,
2791
- });
2792
- if (!buildTransition.ok) {
2793
- return res.status(buildTransition.status).json({
2794
- error: buildTransition.error,
2795
- ...(buildTransition.blockers ? { blockers: buildTransition.blockers } : {}),
2796
- });
2797
- }
2798
- movedTickets.push(buildTransition.ticket);
2799
- }
2800
- emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
2801
- res.json(serializeBundleDetails(result.project, bundle, {
2802
- target_ticket_id: movedTickets[0]?.id ?? null,
2803
- }));
2804
- for (const [index, movedTicket] of movedTickets.entries()) {
2805
- if (!movedTicket.project_id || !movedTicket.worktree_bundle_id) {
2806
- continue;
2807
- }
2808
- const queuedBuild = enqueueBundledBuild({
2809
- ticketId: movedTicket.id,
2810
- projectId: movedTicket.project_id,
2811
- worktreeBundleId: movedTicket.worktree_bundle_id,
2812
- activate: index === 0 && shouldStartBundleBuildNow(movedTicket),
2842
+ const buildResult = buildBundleTickets(result.project.id, req.params.id, io);
2843
+ if (!buildResult.ok) {
2844
+ return res.status(buildResult.status).json({
2845
+ error: buildResult.error,
2846
+ ...('details' in buildResult ? buildResult.details : {}),
2813
2847
  });
2814
- if (queuedBuild.shouldStartNow) {
2815
- startBuild(movedTicket, io);
2816
- }
2817
2848
  }
2849
+ res.status(buildResult.status).json(buildResult.value);
2850
+ buildResult.afterResponse?.();
2818
2851
  });
2819
2852
  app.post('/bundles/:id/review', (req, res) => {
2820
2853
  const result = getRequiredProject(req);
@@ -2959,20 +2992,47 @@ app.post('/projects/validate-repo-path', (req, res) => {
2959
2992
  if (typeof req.body?.repo_path !== 'string' || req.body.repo_path.trim() === '') {
2960
2993
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
2961
2994
  }
2962
- const repoResult = validateRepoPath(req.body.repo_path);
2963
- if ('error' in repoResult) {
2995
+ const repoAnalysis = analyzeRepoPath(req.body.repo_path, { allowInitialization: true });
2996
+ if (repoAnalysis.kind === 'invalid') {
2964
2997
  return res.json({
2965
2998
  valid: false,
2966
2999
  repo_root: null,
2967
- error: repoResult.error,
3000
+ error: repoAnalysis.error,
3001
+ will_initialize: false,
3002
+ has_initial_commit: null,
3003
+ });
3004
+ }
3005
+ if (repoAnalysis.kind === 'initializable') {
3006
+ return res.json({
3007
+ valid: false,
3008
+ repo_root: null,
3009
+ error: null,
3010
+ will_initialize: true,
3011
+ has_initial_commit: null,
2968
3012
  });
2969
3013
  }
2970
3014
  return res.json({
2971
3015
  valid: true,
2972
- repo_root: repoResult.value,
3016
+ repo_root: repoAnalysis.repoRoot,
2973
3017
  error: null,
3018
+ will_initialize: false,
3019
+ has_initial_commit: repoAnalysis.hasInitialCommit,
2974
3020
  });
2975
3021
  });
3022
+ app.post('/projects/:id/create-initial-commit', (req, res) => {
3023
+ const project = getProjectById(req.params.id);
3024
+ if (!project) {
3025
+ return res.status(404).json({ error: 'Not found' });
3026
+ }
3027
+ try {
3028
+ createInitialCommit(project.repo_path);
3029
+ }
3030
+ catch (error) {
3031
+ return res.status(409).json({ error: getErrorMessage(error) || 'Unable to create the initial commit right now.' });
3032
+ }
3033
+ const updatedProject = getProjectById(project.id);
3034
+ return res.json(serializeProject(updatedProject ?? null));
3035
+ });
2976
3036
  app.post('/projects/browse-repo-path', (_req, res) => {
2977
3037
  const browseResult = browseRepoPath();
2978
3038
  if (browseResult.status === 'cancelled') {
@@ -3322,12 +3382,23 @@ app.get('/projects', (_req, res) => {
3322
3382
  res.json(projects.map(serializeProject));
3323
3383
  });
3324
3384
  app.post('/projects', (req, res) => {
3325
- const validation = validateProjectPayload(asProjectPayload(req.body));
3385
+ const validation = validateProjectPayload(asProjectPayload(req.body), { allowRepoInitialization: true });
3326
3386
  if ('error' in validation) {
3327
3387
  return res.status(400).json({ error: validation.error });
3328
3388
  }
3329
3389
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
3330
3390
  const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, auto_park_stale_tickets, memory_enabled, worktree_sync, } = validation.values;
3391
+ if (!repo_path) {
3392
+ return res.status(400).json({ error: 'repo_path must be a non-empty string' });
3393
+ }
3394
+ let normalizedRepoPath = repo_path;
3395
+ try {
3396
+ const initializedRepo = initializeGitRepo(repo_path);
3397
+ normalizedRepoPath = initializedRepo.repoRoot;
3398
+ }
3399
+ catch (error) {
3400
+ return res.status(400).json({ error: getErrorMessage(error) || 'Unable to initialize the project repository.' });
3401
+ }
3331
3402
  const id = randomUUID();
3332
3403
  db.prepare(`
3333
3404
  INSERT INTO projects (
@@ -3349,7 +3420,7 @@ app.post('/projects', (req, res) => {
3349
3420
  worktree_sync
3350
3421
  )
3351
3422
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3352
- `).run(id, name, repo_path, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
3423
+ `).run(id, name, normalizedRepoPath, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, worktree_sync ?? null);
3353
3424
  replaceProjectLinks(id, linked_project_ids ?? []);
3354
3425
  ensureProjectRootBundle(id);
3355
3426
  if (projectCount === 0) {