@open-agent-toolkit/cli 0.2.49 → 0.2.51
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/tool-packs.md +35 -24
- package/assets/docs/reference/cli-reference.md +20 -5
- package/assets/docs/reference/file-locations.md +8 -2
- package/assets/docs/reference/oat-directory-structure.md +14 -5
- package/assets/docs/reference/troubleshooting.md +22 -4
- package/assets/docs/workflows/projects/artifacts.md +9 -4
- package/assets/docs/workflows/projects/lifecycle.md +17 -5
- package/assets/docs/workflows/projects/picking-up-projects.md +26 -9
- package/assets/docs/workflows/projects/pr-flow.md +5 -3
- package/assets/docs/workflows/projects/reviewing-oat-prs.md +12 -8
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +193 -26
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +306 -0
- package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +106 -0
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +110 -0
- package/assets/skills/oat-project-complete/scripts/resolve-synced-archive-entry.mjs +164 -0
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/dist/commands/doctor/index.d.ts +7 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +44 -23
- package/dist/commands/pjm/index.d.ts +0 -2
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +5 -4
- package/dist/commands/pjm/migrate.d.ts +2 -1
- package/dist/commands/pjm/migrate.d.ts.map +1 -1
- package/dist/commands/pjm/migrate.js +23 -17
- package/dist/commands/project/archive/archive-utils.d.ts +8 -1
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +147 -50
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +15 -2
- package/dist/commands/project/links/compute.d.ts +1 -0
- package/dist/commands/project/links/compute.d.ts.map +1 -1
- package/dist/commands/project/links/compute.js +5 -4
- package/dist/commands/project/links/index.d.ts +9 -0
- package/dist/commands/project/links/index.d.ts.map +1 -1
- package/dist/commands/project/links/index.js +71 -2
- package/dist/commands/project/list.d.ts +10 -2
- package/dist/commands/project/list.d.ts.map +1 -1
- package/dist/commands/project/list.js +205 -29
- package/dist/commands/project/open/index.d.ts +2 -0
- package/dist/commands/project/open/index.d.ts.map +1 -1
- package/dist/commands/project/open/index.js +16 -1
- package/dist/commands/project/prune/index.d.ts +10 -4
- package/dist/commands/project/prune/index.d.ts.map +1 -1
- package/dist/commands/project/prune/index.js +85 -15
- package/dist/commands/project/pull/index.d.ts +4 -0
- package/dist/commands/project/pull/index.d.ts.map +1 -1
- package/dist/commands/project/pull/index.js +82 -3
- package/dist/commands/project/sync/git.d.ts +9 -0
- package/dist/commands/project/sync/git.d.ts.map +1 -1
- package/dist/commands/project/sync/git.js +18 -0
- package/dist/commands/project/sync/ref-sync.d.ts +16 -1
- package/dist/commands/project/sync/ref-sync.d.ts.map +1 -1
- package/dist/commands/project/sync/ref-sync.js +288 -17
- package/dist/commands/project/sync/resolve-target.d.ts +10 -0
- package/dist/commands/project/sync/resolve-target.d.ts.map +1 -1
- package/dist/commands/project/sync/resolve-target.js +56 -1
- package/dist/commands/shared/project-scope.d.ts +2 -0
- package/dist/commands/shared/project-scope.d.ts.map +1 -1
- package/dist/commands/shared/project-scope.js +5 -0
- package/dist/commands/state/generate.d.ts +2 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +93 -28
- package/dist/commands/status/index.d.ts +3 -1
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +77 -10
- package/dist/commands/tools/shared/pack-inventory.d.ts +10 -0
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-inventory.js +71 -21
- package/package.json +2 -2
|
@@ -2,11 +2,12 @@ import { lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile, } from
|
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { ensureScopedRootGitignore } from '../../init/gitignore.js';
|
|
4
4
|
import { getFrontmatterBlock } from '../../shared/frontmatter.js';
|
|
5
|
-
import { canonicalizePath, isSyncedCheckout, resolveScopeRoot, SYNCED_REMOTE, syncedRefName, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
5
|
+
import { canonicalizePath, completedSyncedRefName, isSyncedCheckout, resolveScopeRoot, SYNCED_REMOTE, syncedRefName, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
6
6
|
import { readOatLocalConfig, writeOatLocalConfig } from '../../../config/oat-config.js';
|
|
7
7
|
import { CliError } from '../../../errors/cli-error.js';
|
|
8
8
|
import { copyDirectory } from '../../../fs/io.js';
|
|
9
9
|
import YAML from 'yaml';
|
|
10
|
+
import { isAtomicPushUnsupported, pushAtomicRefTransition, } from './git.js';
|
|
10
11
|
import { buildSyncedRecord, readSyncedRecord, writeSyncedRecord, } from './record.js';
|
|
11
12
|
export function classifyRemoteRefLookup(result, remote, ref) {
|
|
12
13
|
if (result.code === 0)
|
|
@@ -442,6 +443,32 @@ async function prepareAdoptionRecord(target, result, adoptionRecord, now, additi
|
|
|
442
443
|
pendingRecordPaths: [recordPath, ...additionalPendingPaths],
|
|
443
444
|
};
|
|
444
445
|
}
|
|
446
|
+
async function probeChildRemoteRefs(target, git) {
|
|
447
|
+
const completedRef = completedSyncedRefName(target.slug);
|
|
448
|
+
const lookup = await git.run(['ls-remote', '--exit-code', target.remote, target.ref, completedRef], { cwd: target.repoRoot, allowFailure: true });
|
|
449
|
+
if (classifyRemoteRefLookup(lookup, target.remote, `${target.ref} and ${completedRef}`) === 'absent') {
|
|
450
|
+
return { activeSha: null, completedSha: null };
|
|
451
|
+
}
|
|
452
|
+
const expectedRefs = new Set([target.ref, completedRef]);
|
|
453
|
+
const advertised = new Map();
|
|
454
|
+
for (const row of lookup.stdout.split('\n').filter(Boolean)) {
|
|
455
|
+
const fields = row.trim().split(/\s+/);
|
|
456
|
+
if (fields.length !== 2 ||
|
|
457
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(fields[0] ?? '') ||
|
|
458
|
+
!expectedRefs.has(fields[1] ?? '') ||
|
|
459
|
+
advertised.has(fields[1] ?? '')) {
|
|
460
|
+
throw new CliError(`Unable to verify terminal refs for child ${target.slug}: origin returned a malformed ref advertisement.`, 2);
|
|
461
|
+
}
|
|
462
|
+
advertised.set(fields[1], fields[0]);
|
|
463
|
+
}
|
|
464
|
+
if (advertised.size === 0) {
|
|
465
|
+
throw new CliError(`Unable to verify terminal refs for child ${target.slug}: origin returned a malformed empty advertisement.`, 2);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
activeSha: advertised.get(target.ref) ?? null,
|
|
469
|
+
completedSha: advertised.get(completedRef) ?? null,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
445
472
|
export async function pullChildren(parentTarget, git) {
|
|
446
473
|
await assertCanonicalSyncTargetIdentity(parentTarget);
|
|
447
474
|
const statePath = resolve(parentTarget.projectPath, 'state.md');
|
|
@@ -477,11 +504,9 @@ export async function pullChildren(parentTarget, git) {
|
|
|
477
504
|
};
|
|
478
505
|
try {
|
|
479
506
|
await assertCanonicalSyncTargetIdentity(childTarget);
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
remote.stdout.trim() === '' &&
|
|
484
|
-
remote.stderr.trim() === '') {
|
|
507
|
+
const completedRef = completedSyncedRefName(slug);
|
|
508
|
+
const { activeSha, completedSha } = await probeChildRemoteRefs(childTarget, git);
|
|
509
|
+
if (activeSha === null && completedSha === null) {
|
|
485
510
|
results.push({
|
|
486
511
|
slug,
|
|
487
512
|
status: 'missing',
|
|
@@ -490,15 +515,27 @@ export async function pullChildren(parentTarget, git) {
|
|
|
490
515
|
});
|
|
491
516
|
continue;
|
|
492
517
|
}
|
|
493
|
-
if (
|
|
518
|
+
if (activeSha !== null &&
|
|
519
|
+
completedSha !== null &&
|
|
520
|
+
activeSha !== completedSha) {
|
|
521
|
+
results.push({
|
|
522
|
+
slug,
|
|
523
|
+
status: 'error',
|
|
524
|
+
message: `Invalid terminal refs for child ${slug}: active ${childTarget.ref} is ${activeSha}, completed ${completedRef} is ${completedSha}. Repair the ref mismatch before retrying; the child was not pulled.`,
|
|
525
|
+
exitCode: 1,
|
|
526
|
+
});
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (completedSha !== null) {
|
|
494
530
|
results.push({
|
|
495
531
|
slug,
|
|
496
532
|
status: 'error',
|
|
497
|
-
message: `
|
|
498
|
-
exitCode:
|
|
533
|
+
message: `Synced child ${slug} is already archived at ${completedRef}@${completedSha} and will not be pulled.${activeSha === completedSha ? ` The matching active ref ${childTarget.ref} is only an inert alias.` : ''}`,
|
|
534
|
+
exitCode: 1,
|
|
499
535
|
});
|
|
500
536
|
continue;
|
|
501
537
|
}
|
|
538
|
+
const adoptionRecord = await classifyAdoptionRecord(childTarget, git);
|
|
502
539
|
const result = await pullSynced(childTarget, git, {
|
|
503
540
|
adopt: adoptionRecord !== 'durable',
|
|
504
541
|
adoptionRecord,
|
|
@@ -613,6 +650,216 @@ async function removePreflightedSyncedCheckout(target, git, force) {
|
|
|
613
650
|
await git.run(['worktree', 'prune'], { cwd: target.repoRoot });
|
|
614
651
|
return { status: 'removed' };
|
|
615
652
|
}
|
|
653
|
+
async function probeTerminalRefs(target, expectedSha, git) {
|
|
654
|
+
const { probeSyncedTerminalRefs } = await import('./resolve-target.js');
|
|
655
|
+
return probeSyncedTerminalRefs(target, expectedSha, git);
|
|
656
|
+
}
|
|
657
|
+
function assertRetirementProbe(target, expectedSha, probe) {
|
|
658
|
+
if (probe.state === 'wrong-sha') {
|
|
659
|
+
if (probe.completedSha !== null && probe.completedSha !== expectedSha) {
|
|
660
|
+
throw new CliError(`Refusing to retire ${target.slug}: completed ref ${probe.completedRef} points to a different SHA (${probe.completedSha}) than the bound source SHA ${expectedSha}.`, 2);
|
|
661
|
+
}
|
|
662
|
+
throw new CliError(`Refusing to retire ${target.slug}: active source ref ${probe.activeRef} points to ${probe.activeSha ?? 'an unknown SHA'}, not the bound source SHA ${expectedSha}.`, 2);
|
|
663
|
+
}
|
|
664
|
+
if (probe.state === 'absent') {
|
|
665
|
+
throw new CliError(`Refusing to retire ${target.slug}: source ref ${probe.activeRef} is missing and completed ref ${probe.completedRef} does not preserve ${expectedSha}.`, 2);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function isTerminalRetirementProbe(probe) {
|
|
669
|
+
return probe.state === 'completed-only' || probe.state === 'both';
|
|
670
|
+
}
|
|
671
|
+
async function fallbackRemoteRefTransition(target, sourceSha, completedRef, completedExpectedSha, git) {
|
|
672
|
+
const create = await git.run([
|
|
673
|
+
'push',
|
|
674
|
+
`--force-with-lease=${completedRef}:${completedExpectedSha ?? ''}`,
|
|
675
|
+
target.remote,
|
|
676
|
+
`${sourceSha}:${completedRef}`,
|
|
677
|
+
], { cwd: target.repoRoot, allowFailure: true });
|
|
678
|
+
if (create.code !== 0) {
|
|
679
|
+
const afterFailure = await probeTerminalRefs(target, sourceSha, git);
|
|
680
|
+
assertRetirementProbe(target, sourceSha, afterFailure);
|
|
681
|
+
if (isTerminalRetirementProbe(afterFailure))
|
|
682
|
+
return;
|
|
683
|
+
throw new CliError(`Remote ref retirement for ${target.slug} was rejected while creating ${completedRef}: ${create.stderr || create.stdout || 'unknown Git error'}`, 2);
|
|
684
|
+
}
|
|
685
|
+
const afterCreate = await probeTerminalRefs(target, sourceSha, git);
|
|
686
|
+
assertRetirementProbe(target, sourceSha, afterCreate);
|
|
687
|
+
if (!isTerminalRetirementProbe(afterCreate)) {
|
|
688
|
+
throw new CliError(`Remote ref retirement for ${target.slug} did not preserve both refs after completed-ref creation.`, 2);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function transitionRemoteRefs(target, sourceSha, completedRef, completedExpectedSha, git) {
|
|
692
|
+
const result = await pushAtomicRefTransition(git, target.repoRoot, {
|
|
693
|
+
remote: target.remote,
|
|
694
|
+
sourceSha,
|
|
695
|
+
activeRef: target.ref,
|
|
696
|
+
completedRef,
|
|
697
|
+
completedExpectedSha,
|
|
698
|
+
});
|
|
699
|
+
if (result.code === 0)
|
|
700
|
+
return;
|
|
701
|
+
if (isAtomicPushUnsupported(result)) {
|
|
702
|
+
await fallbackRemoteRefTransition(target, sourceSha, completedRef, completedExpectedSha, git);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const afterFailure = await probeTerminalRefs(target, sourceSha, git);
|
|
706
|
+
assertRetirementProbe(target, sourceSha, afterFailure);
|
|
707
|
+
if (!isTerminalRetirementProbe(afterFailure)) {
|
|
708
|
+
throw new CliError(`Remote ref retirement for ${target.slug} was rejected; active ref ${target.ref} was retained: ${result.stderr || result.stdout || 'unknown Git error'}`, 2);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function localRefSha(target, ref, git) {
|
|
712
|
+
const result = await git.run(['show-ref', '--hash', ref], {
|
|
713
|
+
cwd: target.repoRoot,
|
|
714
|
+
allowFailure: true,
|
|
715
|
+
});
|
|
716
|
+
assertExpectedGitResult(`git show-ref --hash ${ref}`, result, [0, 1]);
|
|
717
|
+
return result.code === 0 ? result.stdout : null;
|
|
718
|
+
}
|
|
719
|
+
async function reconcileLocalTerminalRefs(target, sourceSha, completedRef, git) {
|
|
720
|
+
const [activeSha, completedSha] = await Promise.all([
|
|
721
|
+
localRefSha(target, target.ref, git),
|
|
722
|
+
localRefSha(target, completedRef, git),
|
|
723
|
+
]);
|
|
724
|
+
if (activeSha !== null && activeSha !== sourceSha) {
|
|
725
|
+
throw new CliError(`Refusing local ref cleanup for ${target.slug}: ${target.ref} points to ${activeSha}, not ${sourceSha}.`, 2);
|
|
726
|
+
}
|
|
727
|
+
if (completedSha !== null && completedSha !== sourceSha) {
|
|
728
|
+
throw new CliError(`Refusing local ref cleanup for ${target.slug}: ${completedRef} points to ${completedSha}, not ${sourceSha}.`, 2);
|
|
729
|
+
}
|
|
730
|
+
if (completedSha === null) {
|
|
731
|
+
const object = await git.run(['cat-file', '-e', `${sourceSha}^{commit}`], {
|
|
732
|
+
cwd: target.repoRoot,
|
|
733
|
+
allowFailure: true,
|
|
734
|
+
});
|
|
735
|
+
if (object.code !== 0) {
|
|
736
|
+
await git.run([
|
|
737
|
+
'fetch',
|
|
738
|
+
'--no-tags',
|
|
739
|
+
'--no-write-fetch-head',
|
|
740
|
+
target.remote,
|
|
741
|
+
completedRef,
|
|
742
|
+
], { cwd: target.repoRoot });
|
|
743
|
+
const afterFetch = await probeTerminalRefs(target, sourceSha, git);
|
|
744
|
+
assertRetirementProbe(target, sourceSha, afterFetch);
|
|
745
|
+
if (!isTerminalRetirementProbe(afterFetch)) {
|
|
746
|
+
throw new CliError(`Refusing local ref reconciliation for ${target.slug}: remote terminal refs changed while fetching ${completedRef}.`, 2);
|
|
747
|
+
}
|
|
748
|
+
await git.run(['cat-file', '-e', `${sourceSha}^{commit}`], {
|
|
749
|
+
cwd: target.repoRoot,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
await git.run(['update-ref', completedRef, sourceSha], {
|
|
753
|
+
cwd: target.repoRoot,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (activeSha !== null) {
|
|
757
|
+
await git.run(['update-ref', '-d', target.ref, sourceSha], {
|
|
758
|
+
cwd: target.repoRoot,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const [finalActiveSha, finalCompletedSha] = await Promise.all([
|
|
762
|
+
localRefSha(target, target.ref, git),
|
|
763
|
+
localRefSha(target, completedRef, git),
|
|
764
|
+
]);
|
|
765
|
+
if (finalActiveSha !== null || finalCompletedSha !== sourceSha) {
|
|
766
|
+
throw new CliError(`Unable to verify local terminal refs for ${target.slug} after remote retirement.`, 2);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
export async function retireSyncedRef(target, sourceSha, git) {
|
|
770
|
+
await assertCanonicalSyncTargetIdentity(target);
|
|
771
|
+
const initial = await probeTerminalRefs(target, sourceSha, git);
|
|
772
|
+
assertRetirementProbe(target, sourceSha, initial);
|
|
773
|
+
const completedRef = initial.completedRef;
|
|
774
|
+
if (initial.state === 'active-only') {
|
|
775
|
+
await transitionRemoteRefs(target, sourceSha, completedRef, initial.completedSha, git);
|
|
776
|
+
}
|
|
777
|
+
const terminal = await probeTerminalRefs(target, sourceSha, git);
|
|
778
|
+
assertRetirementProbe(target, sourceSha, terminal);
|
|
779
|
+
if (!isTerminalRetirementProbe(terminal)) {
|
|
780
|
+
throw new CliError(`Unable to verify a completed terminal state for ${target.slug}; completed ref ${completedRef} was not safely established.`, 2);
|
|
781
|
+
}
|
|
782
|
+
await reconcileLocalTerminalRefs(target, sourceSha, completedRef, git);
|
|
783
|
+
const activeAliasRetained = terminal.state === 'both';
|
|
784
|
+
return {
|
|
785
|
+
status: isTerminalRetirementProbe(initial) ? 'already-retired' : 'retired',
|
|
786
|
+
state: activeAliasRetained ? 'matching-aliases' : 'completed-only',
|
|
787
|
+
activeAliasRetained,
|
|
788
|
+
activeRef: target.ref,
|
|
789
|
+
completedRef,
|
|
790
|
+
verifiedSha: sourceSha,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function exactRemoteRefSha(result, remote, ref) {
|
|
794
|
+
if (classifyRemoteRefLookup(result, remote, ref) === 'absent')
|
|
795
|
+
return null;
|
|
796
|
+
const rows = result.stdout.split('\n').filter((row) => row.length > 0);
|
|
797
|
+
if (rows.length !== 1) {
|
|
798
|
+
throw new CliError(`Unable to verify ${remote}/${ref}: git ls-remote returned an unexpected number of exact-ref rows.`, 2);
|
|
799
|
+
}
|
|
800
|
+
const fields = rows[0]?.trim().split(/\s+/) ?? [];
|
|
801
|
+
if (fields.length !== 2 ||
|
|
802
|
+
fields[1] !== ref ||
|
|
803
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(fields[0] ?? '')) {
|
|
804
|
+
throw new CliError(`Unable to verify ${remote}/${ref}: git ls-remote returned a malformed or unexpected row.`, 2);
|
|
805
|
+
}
|
|
806
|
+
const [sha] = fields;
|
|
807
|
+
return sha;
|
|
808
|
+
}
|
|
809
|
+
export async function deleteCompletedSyncedRefForPrune(target, git) {
|
|
810
|
+
await assertCanonicalSyncTargetIdentity(target);
|
|
811
|
+
const completedRef = completedSyncedRefName(target.slug);
|
|
812
|
+
const [remote, localSha] = await Promise.all([
|
|
813
|
+
git.run(['ls-remote', '--exit-code', target.remote, completedRef], {
|
|
814
|
+
cwd: target.repoRoot,
|
|
815
|
+
allowFailure: true,
|
|
816
|
+
}),
|
|
817
|
+
localRefSha(target, completedRef, git),
|
|
818
|
+
]);
|
|
819
|
+
const remoteSha = exactRemoteRefSha(remote, target.remote, completedRef);
|
|
820
|
+
if (remoteSha !== null) {
|
|
821
|
+
const deletion = await git.run([
|
|
822
|
+
'push',
|
|
823
|
+
`--force-with-lease=${completedRef}:${remoteSha}`,
|
|
824
|
+
target.remote,
|
|
825
|
+
`:${completedRef}`,
|
|
826
|
+
], { cwd: target.repoRoot, allowFailure: true });
|
|
827
|
+
if (deletion.code !== 0) {
|
|
828
|
+
const afterRejection = exactRemoteRefSha(await git.run(['ls-remote', '--exit-code', target.remote, completedRef], { cwd: target.repoRoot, allowFailure: true }), target.remote, completedRef);
|
|
829
|
+
if (afterRejection !== null) {
|
|
830
|
+
if (afterRejection !== remoteSha) {
|
|
831
|
+
throw new CliError(`Explicit prune stopped because remote completed ref ${completedRef} advanced from observed ${remoteSha} to ${afterRejection}; the advanced ref was retained. Review the new terminal identity and retry the destructive prune.`, 2);
|
|
832
|
+
}
|
|
833
|
+
throw new CliError(`Explicit prune could not delete retained terminal history at ${completedRef}; the local completed ref was retained. Restore access to ${target.remote} and retry the destructive prune: ${deletion.stderr || deletion.stdout || 'unknown Git error'}`, 2);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
const verifiedSha = exactRemoteRefSha(await git.run(['ls-remote', '--exit-code', target.remote, completedRef], { cwd: target.repoRoot, allowFailure: true }), target.remote, completedRef);
|
|
838
|
+
if (verifiedSha !== null) {
|
|
839
|
+
if (verifiedSha !== remoteSha) {
|
|
840
|
+
throw new CliError(`Explicit prune stopped because remote completed ref ${completedRef} advanced from observed ${remoteSha} to ${verifiedSha}; the advanced ref was retained. Review the new terminal identity and retry the destructive prune.`, 2);
|
|
841
|
+
}
|
|
842
|
+
throw new CliError(`Explicit prune could not verify deletion of retained terminal history at ${completedRef}; the local completed ref was retained.`, 2);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (localSha !== null) {
|
|
847
|
+
const localDeletion = await git.run(['update-ref', '-d', completedRef, localSha], { cwd: target.repoRoot, allowFailure: true });
|
|
848
|
+
if (localDeletion.code !== 0) {
|
|
849
|
+
const currentLocalSha = await localRefSha(target, completedRef, git);
|
|
850
|
+
if (currentLocalSha !== null) {
|
|
851
|
+
if (currentLocalSha !== localSha) {
|
|
852
|
+
throw new CliError(`Explicit prune stopped because local completed ref ${completedRef} advanced from observed ${localSha} to ${currentLocalSha}; the advanced ref was retained. Review the new terminal identity and retry the destructive prune.`, 2);
|
|
853
|
+
}
|
|
854
|
+
throw new CliError(`Explicit prune could not delete local completed ref ${completedRef} at observed SHA ${localSha}; the ref was retained. Retry after resolving the local Git error: ${localDeletion.stderr || localDeletion.stdout || 'unknown Git error'}`, 2);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
completedRef,
|
|
860
|
+
deleted: remoteSha !== null || localSha !== null,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
616
863
|
export async function pruneSynced(target, git, options) {
|
|
617
864
|
await assertCanonicalSyncTargetIdentity(target);
|
|
618
865
|
const scopeRelativeSyncedRoot = relative(resolve(target.repoRoot), resolve(target.syncedRoot));
|
|
@@ -678,15 +925,39 @@ export async function pruneSynced(target, git, options) {
|
|
|
678
925
|
throw new CliError(`Refusing to prune ${target.slug}: checkout ${checkout.projectPath} is ${preflight.status}. Push it first or pass --force. Pinned PR links will stop resolving after prune.`, 1);
|
|
679
926
|
}
|
|
680
927
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
928
|
+
if (options.expectedActiveAliasSha) {
|
|
929
|
+
const expectedSha = options.expectedActiveAliasSha;
|
|
930
|
+
const deletion = await git.run([
|
|
931
|
+
'push',
|
|
932
|
+
`--force-with-lease=${target.ref}:${expectedSha}`,
|
|
933
|
+
target.remote,
|
|
934
|
+
`:${target.ref}`,
|
|
935
|
+
], { cwd: target.repoRoot, allowFailure: true });
|
|
936
|
+
const afterDeletion = await probeTerminalRefs(target, expectedSha, git);
|
|
937
|
+
if (afterDeletion.activeSha === null &&
|
|
938
|
+
afterDeletion.completedSha === expectedSha) {
|
|
939
|
+
// A rejected transport may still have applied the deletion. The
|
|
940
|
+
// authoritative postcondition is safe, so cleanup may continue.
|
|
941
|
+
}
|
|
942
|
+
else if (afterDeletion.activeSha !== null &&
|
|
943
|
+
afterDeletion.activeSha !== expectedSha) {
|
|
944
|
+
throw new CliError(`Explicit prune stopped because active alias ${target.ref} advanced from observed ${expectedSha} to ${afterDeletion.activeSha}; the advanced active ref, all checkouts, the discovery record, and completed history were retained. Reconcile the terminal ref mismatch before retrying.`, 1);
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
throw new CliError(`Unable to safely delete matching active alias ${target.ref} at ${expectedSha}; no local checkouts or discovery records were removed and completed history was retained. Restore access to ${target.remote}, verify both terminal refs, and retry explicit prune: ${deletion.stderr || deletion.stdout || 'terminal postcondition was not established'}`, 2);
|
|
948
|
+
}
|
|
685
949
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
950
|
+
else {
|
|
951
|
+
try {
|
|
952
|
+
await git.run(['push', target.remote, `:${target.ref}`], {
|
|
953
|
+
cwd: target.repoRoot,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
958
|
+
const forceFlag = options.force ? ' --force' : '';
|
|
959
|
+
throw new CliError(`Unable to delete remote ref ${target.ref}; no local checkouts were removed. Restore access to ${target.remote}, then retry oat project prune ${shellQuote(target.slug)}${forceFlag}. ${detail}`, 2);
|
|
960
|
+
}
|
|
690
961
|
}
|
|
691
962
|
try {
|
|
692
963
|
for (const checkout of checkouts) {
|
|
@@ -23,5 +23,15 @@ export interface ResolvedSyncTarget extends SyncTarget {
|
|
|
23
23
|
adopt: boolean;
|
|
24
24
|
adoptionRecord: AdoptionRecordState;
|
|
25
25
|
}
|
|
26
|
+
export type SyncedTerminalRefState = 'absent' | 'active-only' | 'completed-only' | 'both' | 'wrong-sha';
|
|
27
|
+
export interface SyncedTerminalRefProbe {
|
|
28
|
+
state: SyncedTerminalRefState;
|
|
29
|
+
activeRef: string;
|
|
30
|
+
completedRef: string;
|
|
31
|
+
expectedSha: string;
|
|
32
|
+
activeSha: string | null;
|
|
33
|
+
completedSha: string | null;
|
|
34
|
+
}
|
|
35
|
+
export declare function probeSyncedTerminalRefs(target: SyncTarget, expectedSha: string, git: GitRunner): Promise<SyncedTerminalRefProbe>;
|
|
26
36
|
export declare function resolveSyncedTarget(context: ResolveSyncedTargetContext, pathOrSlug?: string, overrides?: Partial<ResolveSyncedTargetDependencies>, options?: ResolveSyncedTargetOptions): Promise<ResolvedSyncTarget>;
|
|
27
37
|
//# sourceMappingURL=resolve-target.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/sync/resolve-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAQ,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/sync/resolve-target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAQ,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAQjE,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAIL,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,mBAAmB,CAAC;CACrC;AAED,MAAM,MAAM,sBAAsB,GAC9B,QAAQ,GACR,aAAa,GACb,gBAAgB,GAChB,MAAM,GACN,WAAW,CAAC;AAEhB,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,sBAAsB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AA+DD,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,sBAAsB,CAAC,CA+BjC;AAsFD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,EACnC,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,GAAE,OAAO,CAAC,+BAA+B,CAAM,EACxD,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,kBAAkB,CAAC,CA+H7B"}
|
|
@@ -2,10 +2,65 @@ import { realpath, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
4
4
|
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
5
|
-
import { assertValidProjectSlug, resolveProjectScope, resolveScopeRoot, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
5
|
+
import { assertValidProjectSlug, completedSyncedRefName, resolveProjectScope, resolveScopeRoot, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
6
6
|
import { readOatLocalConfig } from '../../../config/oat-config.js';
|
|
7
7
|
import { CliError } from '../../../errors/cli-error.js';
|
|
8
8
|
import { assertCanonicalSyncTargetIdentity, buildSyncTarget, classifyRemoteRefLookup, classifyAdoptionRecord, } from './ref-sync.js';
|
|
9
|
+
async function remoteTerminalRefShas(target, activeRef, completedRef, git) {
|
|
10
|
+
const result = await git.run(['ls-remote', '--exit-code', target.remote, activeRef, completedRef], { cwd: target.repoRoot, allowFailure: true });
|
|
11
|
+
if (classifyRemoteRefLookup(result, target.remote, `${activeRef} and ${completedRef}`) === 'absent') {
|
|
12
|
+
return { activeSha: null, completedSha: null };
|
|
13
|
+
}
|
|
14
|
+
const requestedRefs = new Set([activeRef, completedRef]);
|
|
15
|
+
const advertised = new Map();
|
|
16
|
+
const rows = result.stdout.split('\n').filter((row) => row.length > 0);
|
|
17
|
+
if (rows.length === 0) {
|
|
18
|
+
throw new CliError(`Unable to verify terminal refs for ${target.slug}: git ls-remote returned a malformed empty advertisement.`, 2);
|
|
19
|
+
}
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
const fields = row.trim().split(/\s+/);
|
|
22
|
+
if (fields.length !== 2 ||
|
|
23
|
+
!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(fields[0] ?? '')) {
|
|
24
|
+
throw new CliError(`Unable to verify terminal refs for ${target.slug}: git ls-remote returned a malformed row.`, 2);
|
|
25
|
+
}
|
|
26
|
+
const [sha, ref] = fields;
|
|
27
|
+
if (!requestedRefs.has(ref)) {
|
|
28
|
+
throw new CliError(`Unable to verify terminal refs for ${target.slug}: git ls-remote returned unexpected ref ${ref}.`, 2);
|
|
29
|
+
}
|
|
30
|
+
if (advertised.has(ref)) {
|
|
31
|
+
throw new CliError(`Unable to verify terminal refs for ${target.slug}: git ls-remote returned duplicate rows for ${ref}.`, 2);
|
|
32
|
+
}
|
|
33
|
+
advertised.set(ref, sha);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
activeSha: advertised.get(activeRef) ?? null,
|
|
37
|
+
completedSha: advertised.get(completedRef) ?? null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function probeSyncedTerminalRefs(target, expectedSha, git) {
|
|
41
|
+
const activeRef = target.ref;
|
|
42
|
+
const completedRef = completedSyncedRefName(target.slug);
|
|
43
|
+
const { activeSha, completedSha } = await remoteTerminalRefShas(target, activeRef, completedRef, git);
|
|
44
|
+
const wrongSha = (activeSha !== null && activeSha !== expectedSha) ||
|
|
45
|
+
(completedSha !== null && completedSha !== expectedSha);
|
|
46
|
+
const state = wrongSha
|
|
47
|
+
? 'wrong-sha'
|
|
48
|
+
: activeSha !== null && completedSha !== null
|
|
49
|
+
? 'both'
|
|
50
|
+
: activeSha !== null
|
|
51
|
+
? 'active-only'
|
|
52
|
+
: completedSha !== null
|
|
53
|
+
? 'completed-only'
|
|
54
|
+
: 'absent';
|
|
55
|
+
return {
|
|
56
|
+
state,
|
|
57
|
+
activeRef,
|
|
58
|
+
completedRef,
|
|
59
|
+
expectedSha,
|
|
60
|
+
activeSha,
|
|
61
|
+
completedSha,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
9
64
|
async function pathExists(path) {
|
|
10
65
|
try {
|
|
11
66
|
await stat(path);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type ProjectScope = 'shared' | 'local' | 'synced';
|
|
2
2
|
export declare const PROJECT_SCOPES: readonly ["shared", "local", "synced"];
|
|
3
3
|
export declare const SYNCED_REF_NAMESPACE = "refs/oat/projects";
|
|
4
|
+
export declare const COMPLETED_SYNCED_REF_NAMESPACE = "refs/oat/completed";
|
|
4
5
|
export declare const SYNCED_REMOTE = "origin";
|
|
5
6
|
export declare function canonicalizePath(path: string): string;
|
|
6
7
|
interface EffectiveConfigResult {
|
|
@@ -16,6 +17,7 @@ export declare function resolveScopeRoot(repoRoot: string, projectsRoot: string,
|
|
|
16
17
|
export declare function resolveProjectScope(projectPath: string, projectsRoot: string, repoRoot: string): ProjectScope | null;
|
|
17
18
|
export declare function assertValidProjectSlug(slug: string, exitCode?: 1 | 2): void;
|
|
18
19
|
export declare function syncedRefName(slug: string): string;
|
|
20
|
+
export declare function completedSyncedRefName(slug: string): string;
|
|
19
21
|
export declare function syncedRecordPath(scopeRoot: string, slug: string): string;
|
|
20
22
|
export declare function isSyncedCheckout(projectPath: string): Promise<boolean>;
|
|
21
23
|
export declare function resolveDefaultScope(repoRoot: string, env: NodeJS.ProcessEnv, dependencies?: Partial<ProjectScopeDependencies>): Promise<ProjectScope>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-scope.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/project-scope.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzD,eAAO,MAAM,cAAc,wCAIiB,CAAC;AAC7C,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AACxD,eAAO,MAAM,aAAa,WAAW,CAAC;AAItC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBrD;AAED,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9C;AAED,UAAU,wBAAwB;IAChC,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACrC;AAMD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,CAKR;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,YAAY,GAClB,MAAM,CAOR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,IAAI,CAmBrB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,CAAC,GAAG,CAAK,GAClB,IAAI,CAON;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5E;AAeD,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,YAAY,GAAE,OAAO,CAAC,wBAAwB,CAAM,GACnD,OAAO,CAAC,YAAY,CAAC,CAqBvB"}
|
|
1
|
+
{"version":3,"file":"project-scope.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/project-scope.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzD,eAAO,MAAM,cAAc,wCAIiB,CAAC;AAC7C,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AACxD,eAAO,MAAM,8BAA8B,uBAAuB,CAAC;AACnE,eAAO,MAAM,aAAa,WAAW,CAAC;AAItC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBrD;AAED,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC9C;AAED,UAAU,wBAAwB;IAChC,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACrC;AAMD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,CAKR;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,YAAY,GAClB,MAAM,CAOR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,IAAI,CAmBrB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,CAAC,GAAG,CAAK,GAClB,IAAI,CAON;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5E;AAeD,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,YAAY,GAAE,OAAO,CAAC,wBAAwB,CAAM,GACnD,OAAO,CAAC,YAAY,CAAC,CAqBvB"}
|
|
@@ -10,6 +10,7 @@ export const PROJECT_SCOPES = [
|
|
|
10
10
|
'synced',
|
|
11
11
|
];
|
|
12
12
|
export const SYNCED_REF_NAMESPACE = 'refs/oat/projects';
|
|
13
|
+
export const COMPLETED_SYNCED_REF_NAMESPACE = 'refs/oat/completed';
|
|
13
14
|
export const SYNCED_REMOTE = 'origin';
|
|
14
15
|
const PROJECT_SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
15
16
|
export function canonicalizePath(path) {
|
|
@@ -69,6 +70,10 @@ export function syncedRefName(slug) {
|
|
|
69
70
|
assertValidProjectSlug(slug);
|
|
70
71
|
return `${SYNCED_REF_NAMESPACE}/${slug}`;
|
|
71
72
|
}
|
|
73
|
+
export function completedSyncedRefName(slug) {
|
|
74
|
+
assertValidProjectSlug(slug);
|
|
75
|
+
return `${COMPLETED_SYNCED_REF_NAMESPACE}/${slug}`;
|
|
76
|
+
}
|
|
72
77
|
export function syncedRecordPath(scopeRoot, slug) {
|
|
73
78
|
assertValidProjectSlug(slug);
|
|
74
79
|
return join(scopeRoot, `${slug}.json`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GitRunner } from '../project/sync/git.js';
|
|
1
2
|
export interface GitOperations {
|
|
2
3
|
isGitRepo(root: string): boolean;
|
|
3
4
|
diffFileCount(root: string, sha: string): number;
|
|
@@ -7,6 +8,7 @@ export interface GenerateStateOptions {
|
|
|
7
8
|
env?: NodeJS.ProcessEnv;
|
|
8
9
|
today?: string;
|
|
9
10
|
git?: GitOperations;
|
|
11
|
+
gitRunner?: GitRunner;
|
|
10
12
|
}
|
|
11
13
|
export interface GenerateStateResult {
|
|
12
14
|
dashboardPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/state/generate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/state/generate.ts"],"names":[],"mappings":"AAQA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAU9E,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CAClD;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAiHD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvE;AAsgBD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAkF9B"}
|