@open-agent-toolkit/cli 0.2.40 → 0.2.45
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/agents/oat-codebase-mapper.md +15 -3
- package/assets/agents/oat-phase-implementer.md +34 -9
- package/assets/agents/oat-reviewer.md +2 -2
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +11 -1
- package/assets/docs/cli-utilities/tool-packs.md +8 -0
- package/assets/docs/contributing/skills.md +67 -14
- package/assets/docs/quickstart.md +9 -0
- package/assets/docs/reference/cli-reference.md +56 -1
- package/assets/docs/reference/file-locations.md +18 -1
- package/assets/docs/reference/oat-directory-structure.md +20 -4
- package/assets/docs/workflows/projects/artifacts.md +27 -1
- package/assets/docs/workflows/projects/implementation-execution.md +22 -0
- package/assets/docs/workflows/projects/index.md +6 -0
- package/assets/docs/workflows/projects/lifecycle.md +19 -5
- package/assets/docs/workflows/projects/picking-up-projects.md +75 -0
- package/assets/docs/workflows/projects/pr-flow.md +22 -1
- package/assets/docs/workflows/projects/reviewing-oat-prs.md +60 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/analyze/SKILL.md +13 -5
- package/assets/skills/compare/SKILL.md +11 -2
- package/assets/skills/oat-brainstorm/SKILL.md +135 -32
- package/assets/skills/oat-brainstorm/references/destinations.md +9 -6
- package/assets/skills/oat-cursor-cloud-projects/SKILL.md +11 -1
- package/assets/skills/oat-dispatch-subagents/SKILL.md +19 -6
- package/assets/skills/oat-doctor/SKILL.md +8 -1
- package/assets/skills/oat-project-autonomous/SKILL.md +45 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +8 -8
- package/assets/skills/oat-project-capture/SKILL.md +29 -9
- package/assets/skills/oat-project-complete/SKILL.md +610 -38
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +265 -0
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +764 -0
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +134 -0
- package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs +160 -0
- package/assets/skills/oat-project-design/SKILL.md +33 -9
- package/assets/skills/oat-project-discover/SKILL.md +16 -3
- package/assets/skills/oat-project-document/SKILL.md +10 -4
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +8 -8
- package/assets/skills/oat-project-implement/SKILL.md +14 -3
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +18 -6
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +8 -8
- package/assets/skills/oat-project-implement/references/phase-execution.md +9 -3
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +3 -0
- package/assets/skills/oat-project-import-plan/SKILL.md +15 -9
- package/assets/skills/oat-project-new/SKILL.md +7 -5
- package/assets/skills/oat-project-next/SKILL.md +32 -10
- package/assets/skills/oat-project-plan/SKILL.md +18 -3
- package/assets/skills/oat-project-pr-final/SKILL.md +23 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +8 -8
- package/assets/skills/oat-project-pr-progress/SKILL.md +23 -2
- package/assets/skills/oat-project-progress/SKILL.md +23 -9
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +20 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +48 -18
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +8 -8
- package/assets/skills/oat-project-reconcile/SKILL.md +14 -10
- package/assets/skills/oat-project-retro/SKILL.md +3 -3
- package/assets/skills/oat-project-retro/references/apply-procedure.md +102 -6
- package/assets/skills/oat-project-retro-file/SKILL.md +57 -6
- package/assets/skills/oat-project-review-provide/SKILL.md +56 -6
- package/assets/skills/oat-project-review-receive/SKILL.md +16 -7
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +9 -3
- package/assets/skills/oat-project-revise/SKILL.md +12 -3
- package/assets/skills/oat-project-spec/SKILL.md +16 -3
- package/assets/skills/oat-project-summary/SKILL.md +113 -13
- package/assets/skills/oat-repo-improve/SKILL.md +18 -7
- package/assets/skills/oat-review-provide-remote/SKILL.md +11 -2
- package/assets/skills/oat-wave-execute/SKILL.md +34 -2
- package/assets/skills/oat-worktree-bootstrap/SKILL.md +13 -1
- package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +13 -1
- package/dist/commands/config/index.d.ts +1 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +30 -2
- package/dist/commands/doctor/index.d.ts +2 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +27 -3
- package/dist/commands/doctor/synced-projects.d.ts +12 -0
- package/dist/commands/doctor/synced-projects.d.ts.map +1 -0
- package/dist/commands/doctor/synced-projects.js +208 -0
- package/dist/commands/init/gitattributes.d.ts +4 -0
- package/dist/commands/init/gitattributes.d.ts.map +1 -0
- package/dist/commands/init/gitattributes.js +12 -0
- package/dist/commands/init/gitignore.d.ts +8 -1
- package/dist/commands/init/gitignore.d.ts.map +1 -1
- package/dist/commands/init/gitignore.js +146 -41
- package/dist/commands/init/index.d.ts +2 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +3 -0
- package/dist/commands/init/managed-block.d.ts +10 -0
- package/dist/commands/init/managed-block.d.ts.map +1 -0
- package/dist/commands/init/managed-block.js +40 -0
- package/dist/commands/init/tools/workflows/install-workflows.js +1 -1
- package/dist/commands/local/index.d.ts.map +1 -1
- package/dist/commands/local/index.js +5 -1
- package/dist/commands/local/sync.d.ts +4 -0
- package/dist/commands/local/sync.d.ts.map +1 -1
- package/dist/commands/local/sync.js +123 -2
- package/dist/commands/pjm/adoption.d.ts +2 -0
- package/dist/commands/pjm/adoption.d.ts.map +1 -1
- package/dist/commands/pjm/adoption.js +1 -1
- package/dist/commands/project/archive/archive-utils.d.ts +24 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +349 -22
- package/dist/commands/project/archive/index.d.ts.map +1 -1
- package/dist/commands/project/archive/index.js +1 -0
- package/dist/commands/project/archive/push-runner.d.ts +5 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +33 -4
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +3 -1
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +12 -0
- package/dist/commands/project/links/compute.d.ts +9 -0
- package/dist/commands/project/links/compute.d.ts.map +1 -0
- package/dist/commands/project/links/compute.js +27 -0
- package/dist/commands/project/links/index.d.ts +17 -0
- package/dist/commands/project/links/index.d.ts.map +1 -0
- package/dist/commands/project/links/index.js +72 -0
- package/dist/commands/project/links/refresh.d.ts +26 -0
- package/dist/commands/project/links/refresh.d.ts.map +1 -0
- package/dist/commands/project/links/refresh.js +92 -0
- package/dist/commands/project/links/render.d.ts +24 -0
- package/dist/commands/project/links/render.d.ts.map +1 -0
- package/dist/commands/project/links/render.js +68 -0
- package/dist/commands/project/list.d.ts +5 -0
- package/dist/commands/project/list.d.ts.map +1 -1
- package/dist/commands/project/list.js +182 -9
- package/dist/commands/project/migrate/index.d.ts +22 -0
- package/dist/commands/project/migrate/index.d.ts.map +1 -0
- package/dist/commands/project/migrate/index.js +79 -0
- package/dist/commands/project/new/index.d.ts +2 -0
- package/dist/commands/project/new/index.d.ts.map +1 -1
- package/dist/commands/project/new/index.js +14 -2
- package/dist/commands/project/new/scaffold.d.ts +22 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +287 -69
- package/dist/commands/project/open/index.d.ts +16 -0
- package/dist/commands/project/open/index.d.ts.map +1 -1
- package/dist/commands/project/open/index.js +151 -19
- package/dist/commands/project/pause/index.d.ts +8 -0
- package/dist/commands/project/pause/index.d.ts.map +1 -1
- package/dist/commands/project/pause/index.js +129 -19
- package/dist/commands/project/prune/index.d.ts +21 -0
- package/dist/commands/project/prune/index.d.ts.map +1 -0
- package/dist/commands/project/prune/index.js +95 -0
- package/dist/commands/project/pull/index.d.ts +28 -0
- package/dist/commands/project/pull/index.d.ts.map +1 -0
- package/dist/commands/project/pull/index.js +157 -0
- package/dist/commands/project/push/index.d.ts +22 -0
- package/dist/commands/project/push/index.d.ts.map +1 -0
- package/dist/commands/project/push/index.js +100 -0
- package/dist/commands/project/scope/index.d.ts +17 -0
- package/dist/commands/project/scope/index.d.ts.map +1 -0
- package/dist/commands/project/scope/index.js +107 -0
- package/dist/commands/project/split/run.d.ts +4 -0
- package/dist/commands/project/split/run.d.ts.map +1 -1
- package/dist/commands/project/split/run.js +48 -10
- package/dist/commands/project/sync/git.d.ts +18 -0
- package/dist/commands/project/sync/git.d.ts.map +1 -0
- package/dist/commands/project/sync/git.js +60 -0
- package/dist/commands/project/sync/record.d.ts +22 -0
- package/dist/commands/project/sync/record.d.ts.map +1 -0
- package/dist/commands/project/sync/record.js +133 -0
- package/dist/commands/project/sync/ref-sync.d.ts +120 -0
- package/dist/commands/project/sync/ref-sync.d.ts.map +1 -0
- package/dist/commands/project/sync/ref-sync.js +962 -0
- package/dist/commands/project/sync/resolve-target.d.ts +27 -0
- package/dist/commands/project/sync/resolve-target.d.ts.map +1 -0
- package/dist/commands/project/sync/resolve-target.js +159 -0
- package/dist/commands/shared/oat-paths.d.ts +4 -1
- package/dist/commands/shared/oat-paths.d.ts.map +1 -1
- package/dist/commands/shared/oat-paths.js +2 -2
- package/dist/commands/shared/project-scope.d.ts +23 -0
- package/dist/commands/shared/project-scope.d.ts.map +1 -0
- package/dist/commands/shared/project-scope.js +106 -0
- package/dist/commands/tools/migrate/index.js +1 -1
- package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-inventory.js +11 -0
- package/dist/commands/tools/shared/pack-lifecycle.js +1 -1
- package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-reconcile.js +4 -1
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +20 -10
- package/dist/commands/tools/update/update-tools.d.ts +2 -0
- package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
- package/dist/config/oat-config.d.ts +6 -3
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +41 -7
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +2 -1
- package/dist/fs/io.d.ts +2 -1
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +5 -2
- package/dist/projects/split/finalize.d.ts.map +1 -1
- package/dist/projects/split/finalize.js +55 -2
- package/dist/projects/split/resume.d.ts +3 -0
- package/dist/projects/split/resume.d.ts.map +1 -1
- package/dist/projects/split/resume.js +34 -7
- package/dist/projects/split/seed-children.d.ts.map +1 -1
- package/dist/projects/split/seed-children.js +6 -0
- package/dist/projects/split/write-parent.d.ts +7 -0
- package/dist/projects/split/write-parent.d.ts.map +1 -1
- package/dist/projects/split/write-parent.js +7 -4
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +1 -0
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +434 -0
- package/package.json +4 -4
- package/dist/__tests__/skills/split-flow-fixtures.d.ts +0 -50
- package/dist/__tests__/skills/split-flow-fixtures.d.ts.map +0 -1
- package/dist/__tests__/skills/split-flow-fixtures.js +0 -161
- package/dist/commands/__tests__/helpers.d.ts +0 -12
- package/dist/commands/__tests__/helpers.d.ts.map +0 -1
- package/dist/commands/__tests__/helpers.js +0 -36
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { readFile as defaultReadFile, writeFile as defaultWriteFile, } from 'node:fs/promises';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
4
|
+
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
5
|
+
import { readSyncedRecord } from '../../project/sync/record.js';
|
|
6
|
+
import { commitRecordChange as defaultCommitRecordChange, pullSynced as defaultPullSynced, pushSynced as defaultPushSynced, } from '../../project/sync/ref-sync.js';
|
|
7
|
+
import { resolveSyncedTarget, } from '../../project/sync/resolve-target.js';
|
|
4
8
|
import { getFrontmatterBlock, getFrontmatterField, } from '../../shared/frontmatter.js';
|
|
5
9
|
import { removeFrontmatterField, replaceFrontmatter, upsertFrontmatterField, } from '../../shared/frontmatter-write.js';
|
|
6
10
|
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
11
|
+
import { PROJECT_SCOPES, resolveProjectScope, resolveScopeRoot, syncedRecordPath, } from '../../shared/project-scope.js';
|
|
7
12
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
8
13
|
import { generateStateDashboard as defaultGenerateStateDashboard, } from '../../state/generate.js';
|
|
9
14
|
import { readOatLocalConfig, setActiveProject, writeOatLocalConfig, } from '../../../config/oat-config.js';
|
|
15
|
+
import { CliError } from '../../../errors/cli-error.js';
|
|
10
16
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
11
17
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
12
18
|
import { assertValidProjectStateFilesystemContent } from '../../../validation/project-state.js';
|
|
@@ -23,14 +29,133 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
23
29
|
writeFile: defaultWriteFile,
|
|
24
30
|
dirExists,
|
|
25
31
|
fileExists,
|
|
32
|
+
readSyncedRecord,
|
|
33
|
+
resolveSyncedTarget,
|
|
34
|
+
pullSynced: defaultPullSynced,
|
|
35
|
+
pushSynced: defaultPushSynced,
|
|
36
|
+
commitRecordChange: defaultCommitRecordChange,
|
|
37
|
+
gitRunner: defaultGitRunner,
|
|
26
38
|
processEnv: process.env,
|
|
27
39
|
now: () => new Date(),
|
|
28
40
|
};
|
|
41
|
+
function pointerPath(repoRoot, absolutePath) {
|
|
42
|
+
const relativePath = relative(repoRoot, absolutePath);
|
|
43
|
+
return relativePath === '..' ||
|
|
44
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
45
|
+
isAbsolute(relativePath)
|
|
46
|
+
? absolutePath
|
|
47
|
+
: relativePath.split(sep).join('/');
|
|
48
|
+
}
|
|
49
|
+
function successfulPull(result) {
|
|
50
|
+
return (result.status === 'created' ||
|
|
51
|
+
result.status === 'updated' ||
|
|
52
|
+
result.status === 'up-to-date');
|
|
53
|
+
}
|
|
54
|
+
async function materializeSyncedProject(repoRoot, target, dependencies) {
|
|
55
|
+
const result = await dependencies.pullSynced(target, dependencies.gitRunner, {
|
|
56
|
+
adopt: target.adopt,
|
|
57
|
+
adoptionRecord: target.adoptionRecord,
|
|
58
|
+
});
|
|
59
|
+
if (!successfulPull(result)) {
|
|
60
|
+
throw new CliError(`Unable to open synced project ${target.slug}: pull ${result.status}.`, 1);
|
|
61
|
+
}
|
|
62
|
+
if (result.pendingRecordPaths?.length) {
|
|
63
|
+
await dependencies.commitRecordChange(repoRoot, result.pendingRecordPaths, `chore(oat): adopt synced project ${target.slug}`, dependencies.gitRunner, { projectRoots: target });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function resolveProject(repoRoot, projectsRoot, input, dependencies) {
|
|
67
|
+
const sharedRoot = resolveScopeRoot(repoRoot, projectsRoot, 'shared');
|
|
68
|
+
const explicit = isAbsolute(input) || input.includes('/');
|
|
69
|
+
if (explicit) {
|
|
70
|
+
const fullProjectPath = isAbsolute(input)
|
|
71
|
+
? resolve(input)
|
|
72
|
+
: resolve(repoRoot, input);
|
|
73
|
+
const scope = resolveProjectScope(fullProjectPath, sharedRoot, repoRoot);
|
|
74
|
+
if (!scope) {
|
|
75
|
+
throw new CliError(`Project path is outside configured scope roots: ${input}`, 1);
|
|
76
|
+
}
|
|
77
|
+
const projectName = basename(fullProjectPath);
|
|
78
|
+
let syncTarget;
|
|
79
|
+
if (scope === 'synced') {
|
|
80
|
+
syncTarget = await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, fullProjectPath, {}, { allowMissingCheckout: true });
|
|
81
|
+
if (!(await dependencies.dirExists(fullProjectPath)) ||
|
|
82
|
+
syncTarget.adopt) {
|
|
83
|
+
await materializeSyncedProject(repoRoot, syncTarget, dependencies);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (!(await dependencies.dirExists(fullProjectPath))) {
|
|
87
|
+
throw new CliError(`Project not found: ${input}`, 1);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
projectName,
|
|
91
|
+
projectPath: pointerPath(repoRoot, fullProjectPath),
|
|
92
|
+
fullProjectPath,
|
|
93
|
+
scope,
|
|
94
|
+
syncTarget,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const candidates = [];
|
|
98
|
+
for (const scope of PROJECT_SCOPES) {
|
|
99
|
+
const scopeRoot = resolveScopeRoot(repoRoot, projectsRoot, scope);
|
|
100
|
+
const fullProjectPath = join(scopeRoot, input);
|
|
101
|
+
if (await dependencies.dirExists(fullProjectPath)) {
|
|
102
|
+
candidates.push({ scope, fullProjectPath, present: true });
|
|
103
|
+
}
|
|
104
|
+
else if (scope === 'synced' &&
|
|
105
|
+
(await dependencies.readSyncedRecord(syncedRecordPath(scopeRoot, input))) !== null) {
|
|
106
|
+
candidates.push({ scope, fullProjectPath, present: false });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (candidates.length > 1) {
|
|
110
|
+
throw new CliError(`Project name "${input}" is ambiguous across scopes: ${candidates
|
|
111
|
+
.map((candidate) => candidate.scope)
|
|
112
|
+
.join(', ')}. Pass an explicit project path.`, 1);
|
|
113
|
+
}
|
|
114
|
+
let candidate = candidates[0];
|
|
115
|
+
let syncTarget;
|
|
116
|
+
if (!candidate) {
|
|
117
|
+
try {
|
|
118
|
+
syncTarget = await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, input, {}, { allowMissingCheckout: true });
|
|
119
|
+
candidate = {
|
|
120
|
+
scope: 'synced',
|
|
121
|
+
fullProjectPath: syncTarget.projectPath,
|
|
122
|
+
present: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof CliError && error.exitCode === 1) {
|
|
127
|
+
throw new CliError(`Project not found: ${input}`, 1);
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (candidate.scope === 'synced') {
|
|
133
|
+
syncTarget ??= await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, candidate.fullProjectPath, {}, { allowMissingCheckout: true });
|
|
134
|
+
if (!candidate.present || syncTarget.adopt) {
|
|
135
|
+
await materializeSyncedProject(repoRoot, syncTarget, dependencies);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
projectName: input,
|
|
140
|
+
projectPath: pointerPath(repoRoot, candidate.fullProjectPath),
|
|
141
|
+
fullProjectPath: candidate.fullProjectPath,
|
|
142
|
+
scope: candidate.scope,
|
|
143
|
+
syncTarget,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function publishSyncedTransition(target, action, dependencies) {
|
|
147
|
+
const result = await dependencies.pushSynced(target, dependencies.gitRunner, {
|
|
148
|
+
message: `chore(oat): ${action} synced project ${target.slug}`,
|
|
149
|
+
});
|
|
150
|
+
if (result.status !== 'pushed' && result.status !== 'up-to-date') {
|
|
151
|
+
throw new CliError(`Unable to ${action} synced project ${target.slug}: push ${result.status}; active project pointer was not changed.`, 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
29
154
|
async function maybeResumePausedProject(statePath, projectPath, dependencies) {
|
|
30
155
|
const stateContent = await dependencies.readFile(statePath, 'utf8');
|
|
31
156
|
const frontmatter = getFrontmatterBlock(stateContent);
|
|
32
157
|
if (!frontmatter) {
|
|
33
|
-
throw new
|
|
158
|
+
throw new CliError(`state.md is missing frontmatter: ${statePath}`, 1);
|
|
34
159
|
}
|
|
35
160
|
if (getFrontmatterField(frontmatter, 'oat_lifecycle') !== 'paused') {
|
|
36
161
|
return false;
|
|
@@ -41,10 +166,15 @@ async function maybeResumePausedProject(statePath, projectPath, dependencies) {
|
|
|
41
166
|
nextBlock = upsertFrontmatterField(nextBlock, 'oat_project_state_updated', dependencies.now().toISOString(), true).nextBlock;
|
|
42
167
|
if (nextBlock !== frontmatter) {
|
|
43
168
|
const nextContent = replaceFrontmatter(stateContent, nextBlock);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
169
|
+
try {
|
|
170
|
+
await assertValidProjectStateFilesystemContent(nextContent, {
|
|
171
|
+
filePath: statePath,
|
|
172
|
+
projectPath,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
177
|
+
}
|
|
48
178
|
await dependencies.writeFile(statePath, nextContent, 'utf8');
|
|
49
179
|
}
|
|
50
180
|
return true;
|
|
@@ -53,23 +183,25 @@ async function runProjectOpen(projectName, options, context, dependencies) {
|
|
|
53
183
|
try {
|
|
54
184
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
55
185
|
const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
|
|
56
|
-
const
|
|
57
|
-
const fullProjectPath =
|
|
58
|
-
if (!(await dependencies.dirExists(fullProjectPath))) {
|
|
59
|
-
throw new Error(`Project not found: ${projectName}`);
|
|
60
|
-
}
|
|
186
|
+
const resolved = await resolveProject(repoRoot, projectsRoot, projectName, dependencies);
|
|
187
|
+
const { projectPath, fullProjectPath } = resolved;
|
|
61
188
|
const statePath = join(fullProjectPath, 'state.md');
|
|
62
189
|
if (!(await dependencies.fileExists(statePath))) {
|
|
63
|
-
throw new
|
|
190
|
+
throw new CliError(`Project state.md not found: ${statePath}`, 1);
|
|
64
191
|
}
|
|
65
192
|
const localConfig = await dependencies.readOatLocalConfig(repoRoot);
|
|
66
193
|
const previousActiveProject = localConfig.activeProject ?? null;
|
|
67
|
-
|
|
194
|
+
const resumedFromPaused = await maybeResumePausedProject(statePath, fullProjectPath, dependencies);
|
|
195
|
+
if (resumedFromPaused && resolved.syncTarget) {
|
|
196
|
+
await publishSyncedTransition(resolved.syncTarget, 'resume', dependencies);
|
|
197
|
+
}
|
|
198
|
+
if (previousActiveProject === projectPath && !resumedFromPaused) {
|
|
68
199
|
if (context.json) {
|
|
69
200
|
context.logger.json({
|
|
70
201
|
status: 'ok',
|
|
71
|
-
projectName,
|
|
202
|
+
projectName: resolved.projectName,
|
|
72
203
|
projectPath,
|
|
204
|
+
scope: resolved.scope,
|
|
73
205
|
previousActiveProject,
|
|
74
206
|
resumedFromPaused: false,
|
|
75
207
|
reason: options.reason ?? null,
|
|
@@ -82,7 +214,6 @@ async function runProjectOpen(projectName, options, context, dependencies) {
|
|
|
82
214
|
process.exitCode = 0;
|
|
83
215
|
return;
|
|
84
216
|
}
|
|
85
|
-
const resumedFromPaused = await maybeResumePausedProject(statePath, fullProjectPath, dependencies);
|
|
86
217
|
await dependencies.setActiveProject(repoRoot, projectPath);
|
|
87
218
|
if (localConfig.lastPausedProject === projectPath) {
|
|
88
219
|
const updatedConfig = await dependencies.readOatLocalConfig(repoRoot);
|
|
@@ -95,8 +226,9 @@ async function runProjectOpen(projectName, options, context, dependencies) {
|
|
|
95
226
|
if (context.json) {
|
|
96
227
|
context.logger.json({
|
|
97
228
|
status: 'ok',
|
|
98
|
-
projectName,
|
|
229
|
+
projectName: resolved.projectName,
|
|
99
230
|
projectPath,
|
|
231
|
+
scope: resolved.scope,
|
|
100
232
|
previousActiveProject,
|
|
101
233
|
resumedFromPaused,
|
|
102
234
|
reason: options.reason ?? null,
|
|
@@ -104,10 +236,10 @@ async function runProjectOpen(projectName, options, context, dependencies) {
|
|
|
104
236
|
}
|
|
105
237
|
else {
|
|
106
238
|
if (previousActiveProject) {
|
|
107
|
-
context.logger.info(`Switching from ${basename(previousActiveProject)} to ${projectName}.`);
|
|
239
|
+
context.logger.info(`Switching from ${basename(previousActiveProject)} to ${resolved.projectName}.`);
|
|
108
240
|
}
|
|
109
241
|
else {
|
|
110
|
-
context.logger.info(`Opened project: ${projectName}`);
|
|
242
|
+
context.logger.info(`Opened project: ${resolved.projectName}`);
|
|
111
243
|
}
|
|
112
244
|
if (resumedFromPaused) {
|
|
113
245
|
context.logger.info('Resumed paused project and cleared pause metadata.');
|
|
@@ -126,7 +258,7 @@ async function runProjectOpen(projectName, options, context, dependencies) {
|
|
|
126
258
|
else {
|
|
127
259
|
context.logger.error(message);
|
|
128
260
|
}
|
|
129
|
-
process.exitCode =
|
|
261
|
+
process.exitCode = error instanceof CliError ? error.exitCode : 2;
|
|
130
262
|
}
|
|
131
263
|
}
|
|
132
264
|
export function createProjectOpenCommand(overrides = {}) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readFile as defaultReadFile, writeFile as defaultWriteFile } from 'node:fs/promises';
|
|
2
2
|
import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
|
|
3
|
+
import { type GitRunner } from '../../project/sync/git.js';
|
|
4
|
+
import { type PushResult, type SyncTarget } from '../../project/sync/ref-sync.js';
|
|
5
|
+
import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
|
|
3
6
|
import { type GenerateStateResult } from '../../state/generate.js';
|
|
4
7
|
import { type OatLocalConfig } from '../../../config/oat-config.js';
|
|
5
8
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
@@ -19,6 +22,11 @@ interface ProjectPauseDependencies {
|
|
|
19
22
|
writeFile: typeof defaultWriteFile;
|
|
20
23
|
dirExists: typeof dirExists;
|
|
21
24
|
fileExists: typeof fileExists;
|
|
25
|
+
resolveSyncedTarget: typeof resolveSyncedTarget;
|
|
26
|
+
pushSynced: (target: SyncTarget, git: GitRunner, options: {
|
|
27
|
+
message?: string;
|
|
28
|
+
}) => Promise<PushResult>;
|
|
29
|
+
gitRunner: GitRunner;
|
|
22
30
|
processEnv: NodeJS.ProcessEnv;
|
|
23
31
|
now: () => Date;
|
|
24
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/pause/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/pause/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,EAEpB,MAAM,uCAAuC,CAAC;AAe/C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,kBAAkB,EAAE,CAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,KAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,sBAAsB,EAAE,CAAC,OAAO,EAAE;QAChC,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnC,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,gBAAgB,CAAC;IACnC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,UAAU,EAAE,CACV,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,KAC1B,OAAO,CAAC,UAAU,CAAC,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAwTD,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAsBT"}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readFile as defaultReadFile, writeFile as defaultWriteFile, } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
4
|
+
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
5
|
+
import { pushSynced as defaultPushSynced, } from '../../project/sync/ref-sync.js';
|
|
6
|
+
import { resolveSyncedTarget, } from '../../project/sync/resolve-target.js';
|
|
4
7
|
import { getFrontmatterBlock } from '../../shared/frontmatter.js';
|
|
5
8
|
import { removeFrontmatterField, replaceFrontmatter, upsertFrontmatterField, } from '../../shared/frontmatter-write.js';
|
|
6
9
|
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
10
|
+
import { PROJECT_SCOPES, resolveProjectScope, resolveScopeRoot, } from '../../shared/project-scope.js';
|
|
7
11
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
8
12
|
import { generateStateDashboard as defaultGenerateStateDashboard, } from '../../state/generate.js';
|
|
9
13
|
import { clearActiveProject, readOatLocalConfig, } from '../../../config/oat-config.js';
|
|
14
|
+
import { CliError } from '../../../errors/cli-error.js';
|
|
10
15
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
11
16
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
12
17
|
import { assertValidProjectStateFilesystemContent } from '../../../validation/project-state.js';
|
|
@@ -22,40 +27,118 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
22
27
|
writeFile: defaultWriteFile,
|
|
23
28
|
dirExists,
|
|
24
29
|
fileExists,
|
|
30
|
+
resolveSyncedTarget,
|
|
31
|
+
pushSynced: defaultPushSynced,
|
|
32
|
+
gitRunner: defaultGitRunner,
|
|
25
33
|
processEnv: process.env,
|
|
26
34
|
now: () => new Date(),
|
|
27
35
|
};
|
|
36
|
+
function pointerPath(repoRoot, absolutePath) {
|
|
37
|
+
const relativePath = relative(repoRoot, absolutePath);
|
|
38
|
+
return relativePath === '..' ||
|
|
39
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
40
|
+
isAbsolute(relativePath)
|
|
41
|
+
? absolutePath
|
|
42
|
+
: relativePath.split(sep).join('/');
|
|
43
|
+
}
|
|
44
|
+
async function resolveNamedProject(repoRoot, projectsRoot, input, dependencies) {
|
|
45
|
+
const sharedRoot = resolveScopeRoot(repoRoot, projectsRoot, 'shared');
|
|
46
|
+
const explicit = isAbsolute(input) || input.includes('/');
|
|
47
|
+
if (explicit) {
|
|
48
|
+
const fullProjectPath = isAbsolute(input)
|
|
49
|
+
? resolve(input)
|
|
50
|
+
: resolve(repoRoot, input);
|
|
51
|
+
const scope = resolveProjectScope(fullProjectPath, sharedRoot, repoRoot);
|
|
52
|
+
if (!scope) {
|
|
53
|
+
throw new CliError(`Project path is outside configured scope roots: ${input}`, 1);
|
|
54
|
+
}
|
|
55
|
+
if (!(await dependencies.dirExists(fullProjectPath))) {
|
|
56
|
+
throw new CliError(`Project not found: ${input}`, 1);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
projectName: basename(fullProjectPath),
|
|
60
|
+
projectPath: pointerPath(repoRoot, fullProjectPath),
|
|
61
|
+
fullProjectPath,
|
|
62
|
+
scope,
|
|
63
|
+
syncTarget: scope === 'synced'
|
|
64
|
+
? await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, fullProjectPath)
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const candidates = [];
|
|
69
|
+
for (const scope of PROJECT_SCOPES) {
|
|
70
|
+
const fullProjectPath = join(resolveScopeRoot(repoRoot, projectsRoot, scope), input);
|
|
71
|
+
if (!(await dependencies.dirExists(fullProjectPath)))
|
|
72
|
+
continue;
|
|
73
|
+
candidates.push({
|
|
74
|
+
projectName: input,
|
|
75
|
+
projectPath: pointerPath(repoRoot, fullProjectPath),
|
|
76
|
+
fullProjectPath,
|
|
77
|
+
scope,
|
|
78
|
+
syncTarget: scope === 'synced'
|
|
79
|
+
? await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, fullProjectPath)
|
|
80
|
+
: undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (candidates.length > 1) {
|
|
84
|
+
throw new CliError(`Project name "${input}" is ambiguous across scopes: ${candidates
|
|
85
|
+
.map((candidate) => candidate.scope)
|
|
86
|
+
.join(', ')}. Pass an explicit project path.`, 1);
|
|
87
|
+
}
|
|
88
|
+
if (!candidates[0]) {
|
|
89
|
+
throw new CliError(`Project not found: ${input}`, 1);
|
|
90
|
+
}
|
|
91
|
+
return candidates[0];
|
|
92
|
+
}
|
|
93
|
+
async function publishPausedState(target, dependencies) {
|
|
94
|
+
const result = await dependencies.pushSynced(target, dependencies.gitRunner, {
|
|
95
|
+
message: `chore(oat): pause synced project ${target.slug}`,
|
|
96
|
+
});
|
|
97
|
+
if (result.status !== 'pushed' && result.status !== 'up-to-date') {
|
|
98
|
+
const recovery = result.status === 'conflict'
|
|
99
|
+
? ` Resolve the conflicted files without discarding either side, then run oat project pull ${target.slug} --continue followed by oat project push ${target.slug}, then re-run oat project pause ${target.slug} to finish and clear the active pointer; or run oat project pull ${target.slug} --abort to leave the committed local pause unpublished.`
|
|
100
|
+
: ` The committed local pause was retained with a clean checkout; re-run oat project pause ${target.slug} to retry publication.`;
|
|
101
|
+
throw new PausePublicationError(`Unable to pause synced project ${target.slug}: push ${result.status}; active project pointer was not changed.${recovery}`, result.status);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
class PausePublicationError extends CliError {
|
|
106
|
+
status;
|
|
107
|
+
constructor(message, status) {
|
|
108
|
+
super(message, 1);
|
|
109
|
+
this.status = status;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
28
112
|
async function runProjectPause(projectName, options, context, dependencies) {
|
|
29
113
|
try {
|
|
30
114
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
31
115
|
const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
|
|
32
116
|
const localConfig = await dependencies.readOatLocalConfig(repoRoot);
|
|
33
117
|
const activeProject = localConfig.activeProject ?? null;
|
|
34
|
-
let
|
|
35
|
-
let resolvedProjectName;
|
|
118
|
+
let resolved;
|
|
36
119
|
if (projectName) {
|
|
37
|
-
|
|
38
|
-
resolvedProjectName = projectName;
|
|
120
|
+
resolved = await resolveNamedProject(repoRoot, projectsRoot, projectName, dependencies);
|
|
39
121
|
}
|
|
40
122
|
else {
|
|
41
123
|
if (!activeProject) {
|
|
42
|
-
throw new
|
|
124
|
+
throw new CliError('No project specified and no active project', 1);
|
|
43
125
|
}
|
|
44
|
-
|
|
45
|
-
resolvedProjectName = projectPath.split('/').at(-1) ?? projectPath;
|
|
46
|
-
}
|
|
47
|
-
const fullProjectPath = join(repoRoot, projectPath);
|
|
48
|
-
if (!(await dependencies.dirExists(fullProjectPath))) {
|
|
49
|
-
throw new Error(`Project not found: ${resolvedProjectName}`);
|
|
126
|
+
resolved = await resolveNamedProject(repoRoot, projectsRoot, activeProject, dependencies);
|
|
50
127
|
}
|
|
128
|
+
const { projectName: resolvedProjectName, projectPath, fullProjectPath, } = resolved;
|
|
51
129
|
const statePath = join(fullProjectPath, 'state.md');
|
|
52
130
|
if (!(await dependencies.fileExists(statePath))) {
|
|
53
|
-
throw new
|
|
131
|
+
throw new CliError(`Project state.md not found: ${statePath}`, 1);
|
|
54
132
|
}
|
|
55
133
|
const content = await dependencies.readFile(statePath, 'utf8');
|
|
134
|
+
const prePauseHead = resolved.syncTarget
|
|
135
|
+
? (await dependencies.gitRunner.run(['rev-parse', 'HEAD'], {
|
|
136
|
+
cwd: resolved.syncTarget.projectPath,
|
|
137
|
+
})).stdout
|
|
138
|
+
: null;
|
|
56
139
|
const frontmatter = getFrontmatterBlock(content);
|
|
57
140
|
if (!frontmatter) {
|
|
58
|
-
throw new
|
|
141
|
+
throw new CliError(`state.md is missing frontmatter: ${statePath}`, 1);
|
|
59
142
|
}
|
|
60
143
|
let nextBlock = upsertFrontmatterField(frontmatter, 'oat_lifecycle', 'paused', true).nextBlock;
|
|
61
144
|
const nowIso = dependencies.now().toISOString();
|
|
@@ -69,12 +152,38 @@ async function runProjectPause(projectName, options, context, dependencies) {
|
|
|
69
152
|
}
|
|
70
153
|
if (nextBlock !== frontmatter) {
|
|
71
154
|
const nextContent = replaceFrontmatter(content, nextBlock);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
155
|
+
try {
|
|
156
|
+
await assertValidProjectStateFilesystemContent(nextContent, {
|
|
157
|
+
filePath: statePath,
|
|
158
|
+
projectPath: fullProjectPath,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
163
|
+
}
|
|
76
164
|
await dependencies.writeFile(statePath, nextContent, 'utf8');
|
|
77
165
|
}
|
|
166
|
+
if (resolved.syncTarget) {
|
|
167
|
+
try {
|
|
168
|
+
await publishPausedState(resolved.syncTarget, dependencies);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (nextBlock !== frontmatter &&
|
|
172
|
+
!(error instanceof PausePublicationError)) {
|
|
173
|
+
const pauseCommitRetained = resolved.syncTarget && prePauseHead
|
|
174
|
+
? (await dependencies.gitRunner.run(['rev-parse', 'HEAD'], {
|
|
175
|
+
cwd: resolved.syncTarget.projectPath,
|
|
176
|
+
})).stdout !== prePauseHead
|
|
177
|
+
: false;
|
|
178
|
+
if (pauseCommitRetained && resolved.syncTarget) {
|
|
179
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
180
|
+
throw new PausePublicationError(`Unable to pause synced project ${resolved.syncTarget.slug}: publication failed: ${detail}. Active project pointer was not changed. The committed local pause was retained with a clean checkout; re-run oat project pause ${resolved.syncTarget.slug} to retry publication.`, 'failed');
|
|
181
|
+
}
|
|
182
|
+
await dependencies.writeFile(statePath, content, 'utf8');
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
78
187
|
const pointerCleared = activeProject === projectPath;
|
|
79
188
|
if (pointerCleared) {
|
|
80
189
|
await dependencies.clearActiveProject(repoRoot, {
|
|
@@ -87,6 +196,7 @@ async function runProjectPause(projectName, options, context, dependencies) {
|
|
|
87
196
|
status: 'ok',
|
|
88
197
|
projectName: resolvedProjectName,
|
|
89
198
|
projectPath,
|
|
199
|
+
scope: resolved.scope,
|
|
90
200
|
pointerCleared,
|
|
91
201
|
reason: options.reason ?? null,
|
|
92
202
|
});
|
|
@@ -107,7 +217,7 @@ async function runProjectPause(projectName, options, context, dependencies) {
|
|
|
107
217
|
else {
|
|
108
218
|
context.logger.error(message);
|
|
109
219
|
}
|
|
110
|
-
process.exitCode =
|
|
220
|
+
process.exitCode = error instanceof CliError ? error.exitCode : 2;
|
|
111
221
|
}
|
|
112
222
|
}
|
|
113
223
|
export function createProjectPauseCommand(overrides = {}) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
|
|
2
|
+
import { type GitRunner } from '../../project/sync/git.js';
|
|
3
|
+
import { type PruneResult, type SyncTarget } from '../../project/sync/ref-sync.js';
|
|
4
|
+
import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
interface ProjectPruneDependencies {
|
|
7
|
+
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
8
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
9
|
+
resolveSyncedTarget: typeof resolveSyncedTarget;
|
|
10
|
+
pruneSynced: (target: SyncTarget, git: GitRunner, options: {
|
|
11
|
+
force: boolean;
|
|
12
|
+
commit: boolean;
|
|
13
|
+
}) => Promise<PruneResult>;
|
|
14
|
+
readProjectState: (target: SyncTarget, git: GitRunner) => Promise<string | null>;
|
|
15
|
+
gitRunner: GitRunner;
|
|
16
|
+
processEnv: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
18
|
+
export declare function readPruneProjectState(target: SyncTarget, git: GitRunner): Promise<string | null>;
|
|
19
|
+
export declare function createProjectPruneCommand(overrides?: Partial<ProjectPruneDependencies>): Command;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/prune/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE9E,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAS5E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,WAAW,EAAE,CACX,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KACzC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1B,gBAAgB,EAAE,CAChB,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,KACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB;AAmED,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAmBT"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
|
+
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
5
|
+
import { readSyncedRecord } from '../../project/sync/record.js';
|
|
6
|
+
import { pruneSynced, } from '../../project/sync/ref-sync.js';
|
|
7
|
+
import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
|
|
8
|
+
import { getFrontmatterBlock, parseFrontmatterScalarFields, } from '../../shared/frontmatter.js';
|
|
9
|
+
import { syncedRecordPath } from '../../shared/project-scope.js';
|
|
10
|
+
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
11
|
+
import { CliError } from '../../../errors/cli-error.js';
|
|
12
|
+
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
export async function readPruneProjectState(target, git) {
|
|
15
|
+
try {
|
|
16
|
+
return await readFile(join(target.projectPath, 'state.md'), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// A completed or not-yet-pulled project may have no checkout.
|
|
20
|
+
}
|
|
21
|
+
const fetched = await git.run(['fetch', target.remote, `+${target.ref}:${target.ref}`], { cwd: target.repoRoot, allowFailure: true });
|
|
22
|
+
if (fetched.code === 0) {
|
|
23
|
+
const shown = await git.run(['show', `${target.ref}:state.md`], {
|
|
24
|
+
cwd: target.repoRoot,
|
|
25
|
+
allowFailure: true,
|
|
26
|
+
});
|
|
27
|
+
if (shown.code === 0)
|
|
28
|
+
return shown.stdout;
|
|
29
|
+
}
|
|
30
|
+
const record = await readSyncedRecord(syncedRecordPath(target.syncedRoot, target.slug));
|
|
31
|
+
const snapshot = record?.archiveSnapshot ?? target.slug;
|
|
32
|
+
try {
|
|
33
|
+
return await readFile(join(target.syncedRoot, '..', 'archived', snapshot, 'state.md'), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const DEFAULT_DEPENDENCIES = {
|
|
40
|
+
buildCommandContext,
|
|
41
|
+
resolveProjectRoot,
|
|
42
|
+
resolveSyncedTarget,
|
|
43
|
+
pruneSynced,
|
|
44
|
+
readProjectState: readPruneProjectState,
|
|
45
|
+
gitRunner: defaultGitRunner,
|
|
46
|
+
processEnv: process.env,
|
|
47
|
+
};
|
|
48
|
+
function hasOpenPr(state) {
|
|
49
|
+
const frontmatter = state ? getFrontmatterBlock(state) : null;
|
|
50
|
+
if (!frontmatter)
|
|
51
|
+
return false;
|
|
52
|
+
const parsed = parseFrontmatterScalarFields(frontmatter, ['oat_pr_status']);
|
|
53
|
+
return parsed.valid && parsed.values.oat_pr_status === 'open';
|
|
54
|
+
}
|
|
55
|
+
async function runPrune(context, pathOrSlug, options, dependencies) {
|
|
56
|
+
try {
|
|
57
|
+
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
58
|
+
const target = await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, pathOrSlug, { gitRunner: dependencies.gitRunner }, {
|
|
59
|
+
allowMissingCheckout: true,
|
|
60
|
+
allowStagedPruneDeletion: true,
|
|
61
|
+
});
|
|
62
|
+
const state = await dependencies.readProjectState(target, dependencies.gitRunner);
|
|
63
|
+
if (!options.force && hasOpenPr(state)) {
|
|
64
|
+
throw new CliError(`Refusing to prune ${target.slug} while its PR is open. Pass --force to confirm; pinned links will stop resolving.`, 1);
|
|
65
|
+
}
|
|
66
|
+
context.logger.warn(`Pruning ${target.slug} removes ${target.ref}; pinned links will stop resolving.`);
|
|
67
|
+
const result = await dependencies.pruneSynced(target, dependencies.gitRunner, { force: options.force, commit: options.commit });
|
|
68
|
+
const payload = { ...result, ref: target.ref };
|
|
69
|
+
if (context.json)
|
|
70
|
+
context.logger.json(payload);
|
|
71
|
+
else
|
|
72
|
+
context.logger.info(`Pruned synced project ${target.slug}.`);
|
|
73
|
+
process.exitCode = 0;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
if (context.json)
|
|
78
|
+
context.logger.json({ status: 'error', message });
|
|
79
|
+
else
|
|
80
|
+
context.logger.error(message);
|
|
81
|
+
process.exitCode = error instanceof CliError ? error.exitCode : 2;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function createProjectPruneCommand(overrides = {}) {
|
|
85
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
86
|
+
return new Command('prune')
|
|
87
|
+
.description('Permanently remove a synced project and its ref')
|
|
88
|
+
.argument('[project-path|slug]', 'Synced project path or slug')
|
|
89
|
+
.option('--force', 'Prune despite open PR or unsafe checkout state', false)
|
|
90
|
+
.option('--no-commit', 'Do not commit the discovery record deletion')
|
|
91
|
+
.action(async (pathOrSlug, options, command) => {
|
|
92
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
93
|
+
await runPrune(context, pathOrSlug, options, dependencies);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
|
|
2
|
+
import { type GitRunner } from '../../project/sync/git.js';
|
|
3
|
+
import { commitRecordChange as defaultCommitRecordChange, type PullChildResult, type PullResult, type AdoptionRecordState, type SyncTarget } from '../../project/sync/ref-sync.js';
|
|
4
|
+
import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
interface ProjectPullDependencies {
|
|
7
|
+
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
8
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
9
|
+
resolveSyncedTarget: typeof resolveSyncedTarget;
|
|
10
|
+
pullSynced: (target: SyncTarget, git: GitRunner, options?: {
|
|
11
|
+
adopt?: boolean;
|
|
12
|
+
adoptionRecord?: AdoptionRecordState;
|
|
13
|
+
now?: Date;
|
|
14
|
+
}) => Promise<PullResult>;
|
|
15
|
+
pullChildren: (target: SyncTarget, git: GitRunner) => Promise<PullChildResult[]>;
|
|
16
|
+
commitRecordChange: typeof defaultCommitRecordChange;
|
|
17
|
+
continueSynced: (target: SyncTarget, git: GitRunner, options?: {
|
|
18
|
+
adopt?: boolean;
|
|
19
|
+
adoptionRecord?: AdoptionRecordState;
|
|
20
|
+
now?: Date;
|
|
21
|
+
}) => Promise<PullResult>;
|
|
22
|
+
abortSynced: (target: SyncTarget, git: GitRunner) => Promise<void>;
|
|
23
|
+
gitRunner: GitRunner;
|
|
24
|
+
processEnv: NodeJS.ProcessEnv;
|
|
25
|
+
}
|
|
26
|
+
export declare function createProjectPullCommand(overrides?: Partial<ProjectPullDependencies>): Command;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/pull/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAEL,kBAAkB,IAAI,yBAAyB,EAI/C,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,EAEpB,MAAM,uCAAuC,CAAC;AAI/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,UAAU,EAAE,CACV,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,cAAc,CAAC,EAAE,mBAAmB,CAAC;QACrC,GAAG,CAAC,EAAE,IAAI,CAAC;KACZ,KACE,OAAO,CAAC,UAAU,CAAC,CAAC;IACzB,YAAY,EAAE,CACZ,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,KACX,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAChC,kBAAkB,EAAE,OAAO,yBAAyB,CAAC;IACrD,cAAc,EAAE,CACd,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,cAAc,CAAC,EAAE,mBAAmB,CAAC;QACrC,GAAG,CAAC,EAAE,IAAI,CAAC;KACZ,KACE,OAAO,CAAC,UAAU,CAAC,CAAC;IACzB,WAAW,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAiLD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAqBT"}
|