@open-agent-toolkit/cli 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/assets/agents/oat-reviewer.md +48 -10
- package/assets/docs/cli-utilities/config-and-local-state.md +13 -1
- package/assets/docs/cli-utilities/configuration.md +24 -6
- package/assets/docs/docs-tooling/workflows.md +8 -2
- package/assets/docs/reference/cli-reference.md +13 -7
- package/assets/docs/reference/file-locations.md +1 -1
- package/assets/docs/reference/oat-directory-structure.md +3 -3
- package/assets/docs/workflows/projects/index.md +1 -1
- package/assets/docs/workflows/projects/lifecycle.md +1 -1
- package/assets/docs/workflows/projects/repo-analysis.md +7 -4
- package/assets/docs/workflows/projects/reviews.md +41 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-analyze/SKILL.md +43 -13
- package/assets/skills/oat-docs-analyze/SKILL.md +42 -12
- package/assets/skills/oat-project-complete/SKILL.md +16 -152
- package/assets/skills/oat-project-discover/SKILL.md +22 -4
- package/assets/skills/oat-project-import-plan/SKILL.md +38 -9
- package/assets/skills/oat-project-plan/SKILL.md +30 -7
- package/assets/skills/oat-project-plan-writing/SKILL.md +45 -2
- package/assets/skills/oat-project-progress/SKILL.md +9 -3
- package/assets/skills/oat-project-quick-start/SKILL.md +40 -8
- package/assets/skills/oat-project-review-provide/SKILL.md +24 -11
- package/assets/skills/oat-project-review-receive/SKILL.md +37 -17
- package/assets/skills/oat-wrap-up/SKILL.md +9 -9
- package/assets/skills/oat-wrap-up/references/automation-recipes.md +5 -5
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +38 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/project/archive/archive-utils.d.ts +19 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +94 -25
- package/dist/commands/project/archive/index.d.ts +6 -19
- package/dist/commands/project/archive/index.d.ts.map +1 -1
- package/dist/commands/project/archive/index.js +19 -251
- package/dist/commands/project/archive/push-runner.d.ts +21 -0
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -0
- package/dist/commands/project/archive/push-runner.js +151 -0
- package/dist/commands/project/archive/sync-runner.d.ts +47 -0
- package/dist/commands/project/archive/sync-runner.d.ts.map +1 -0
- package/dist/commands/project/archive/sync-runner.js +232 -0
- package/dist/commands/repo/archive/index.d.ts +4 -0
- package/dist/commands/repo/archive/index.d.ts.map +1 -0
- package/dist/commands/repo/archive/index.js +22 -0
- package/dist/commands/repo/index.d.ts.map +1 -1
- package/dist/commands/repo/index.js +2 -0
- package/dist/commands/review/index.d.ts +3 -0
- package/dist/commands/review/index.d.ts.map +1 -0
- package/dist/commands/review/index.js +7 -0
- package/dist/commands/review/latest.d.ts +23 -0
- package/dist/commands/review/latest.d.ts.map +1 -0
- package/dist/commands/review/latest.js +182 -0
- package/dist/config/oat-config.d.ts +5 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +12 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +4 -0
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
2
|
import { rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, isAbsolute, join, posix as path } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, posix as path, relative, resolve, sep, } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { CliError } from '../../../errors/cli-error.js';
|
|
6
6
|
import { copyDirectory, copySingleFile, dirExists, ensureDir, fileExists, } from '../../../fs/io.js';
|
|
@@ -112,8 +112,32 @@ export function resolveLocalArchiveProjectPath(projectsRoot, projectName) {
|
|
|
112
112
|
const projectsBase = path.dirname(normalizedProjectsRoot);
|
|
113
113
|
return path.join(projectsBase, 'archived', projectName);
|
|
114
114
|
}
|
|
115
|
-
function
|
|
116
|
-
return
|
|
115
|
+
function normalizePathForConfig(pathValue) {
|
|
116
|
+
return pathValue.replaceAll('\\', '/');
|
|
117
|
+
}
|
|
118
|
+
function isInsidePath(parentPath, childPath) {
|
|
119
|
+
const relativePath = relative(parentPath, childPath);
|
|
120
|
+
return (relativePath === '' ||
|
|
121
|
+
(!relativePath.startsWith(`..${sep}`) &&
|
|
122
|
+
relativePath !== '..' &&
|
|
123
|
+
!isAbsolute(relativePath)));
|
|
124
|
+
}
|
|
125
|
+
function resolveArchiveProjectPath(repoRoot, projectsRoot, projectName) {
|
|
126
|
+
const archiveProjectPath = resolveLocalArchiveProjectPath(projectsRoot, projectName);
|
|
127
|
+
if (!isAbsolute(archiveProjectPath)) {
|
|
128
|
+
return archiveProjectPath;
|
|
129
|
+
}
|
|
130
|
+
const resolvedRepoRoot = resolve(repoRoot);
|
|
131
|
+
const resolvedArchiveProjectPath = resolve(archiveProjectPath);
|
|
132
|
+
if (!isInsidePath(resolvedRepoRoot, resolvedArchiveProjectPath)) {
|
|
133
|
+
return resolvedArchiveProjectPath;
|
|
134
|
+
}
|
|
135
|
+
return normalizePathForConfig(relative(resolvedRepoRoot, resolvedArchiveProjectPath));
|
|
136
|
+
}
|
|
137
|
+
function resolveCompletionArchivePath(archiveRepoRoot, archiveProjectPath) {
|
|
138
|
+
return isAbsolute(archiveProjectPath)
|
|
139
|
+
? archiveProjectPath
|
|
140
|
+
: join(archiveRepoRoot, archiveProjectPath);
|
|
117
141
|
}
|
|
118
142
|
function resolveGitPath(repoRoot, gitPath) {
|
|
119
143
|
const normalizedPath = gitPath.trim();
|
|
@@ -150,7 +174,7 @@ async function isGitignoredArchivePath(repoRoot, archiveProjectPath, dependencie
|
|
|
150
174
|
throw error;
|
|
151
175
|
}
|
|
152
176
|
}
|
|
153
|
-
|
|
177
|
+
async function resolvePrimaryRepoRootResolution(repoRoot, dependencies = {}) {
|
|
154
178
|
const execFile = dependencies.gitExecFile ?? execFileAsync;
|
|
155
179
|
try {
|
|
156
180
|
const [{ stdout: commonDir }, { stdout: gitDir }] = await Promise.all([
|
|
@@ -166,30 +190,22 @@ export async function resolvePrimaryRepoRoot(repoRoot, dependencies = {}) {
|
|
|
166
190
|
const resolvedCommonDir = resolveGitPath(repoRoot, commonDir);
|
|
167
191
|
const resolvedGitDir = resolveGitPath(repoRoot, gitDir);
|
|
168
192
|
if (resolvedCommonDir === resolvedGitDir) {
|
|
169
|
-
return repoRoot;
|
|
193
|
+
return { repoRoot, available: true };
|
|
170
194
|
}
|
|
171
195
|
const primaryRepoRoot = dirname(resolvedCommonDir);
|
|
172
196
|
const directoryExists = dependencies.dirExists ?? dirExists;
|
|
173
197
|
if (await directoryExists(primaryRepoRoot)) {
|
|
174
|
-
return primaryRepoRoot;
|
|
198
|
+
return { repoRoot: primaryRepoRoot, available: true };
|
|
175
199
|
}
|
|
200
|
+
return { repoRoot: primaryRepoRoot, available: false };
|
|
176
201
|
}
|
|
177
202
|
catch {
|
|
178
|
-
return repoRoot;
|
|
203
|
+
return { repoRoot, available: false };
|
|
179
204
|
}
|
|
180
|
-
return repoRoot;
|
|
181
205
|
}
|
|
182
|
-
async function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!archivePathIsGitignored) {
|
|
186
|
-
return repoRoot;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return repoRoot;
|
|
191
|
-
}
|
|
192
|
-
return resolvePrimaryRepoRoot(repoRoot, dependencies);
|
|
206
|
+
export async function resolvePrimaryRepoRoot(repoRoot, dependencies = {}) {
|
|
207
|
+
const resolution = await resolvePrimaryRepoRootResolution(repoRoot, dependencies);
|
|
208
|
+
return resolution.available ? resolution.repoRoot : repoRoot;
|
|
193
209
|
}
|
|
194
210
|
async function resolveUniqueArchivePath(archivePath, dependencies) {
|
|
195
211
|
const directoryExists = dependencies.dirExists ?? dirExists;
|
|
@@ -200,6 +216,56 @@ async function resolveUniqueArchivePath(archivePath, dependencies) {
|
|
|
200
216
|
const suffix = timestamp.replace(/[-:TZ.]/g, '').slice(0, 15);
|
|
201
217
|
return `${archivePath}-${suffix}`;
|
|
202
218
|
}
|
|
219
|
+
function buildLocalOnlyArchiveWarning(projectName, archiveProjectPath, primaryRepoRoot) {
|
|
220
|
+
const primaryMessage = primaryRepoRoot
|
|
221
|
+
? `the primary checkout \`${primaryRepoRoot}\` is unavailable`
|
|
222
|
+
: 'the primary checkout could not be resolved';
|
|
223
|
+
return `Refusing to archive project \`${projectName}\` because \`${archiveProjectPath}\` is gitignored in this worktree and ${primaryMessage}. Run \`oat project archive\` from the primary checkout or restore that checkout before retrying.`;
|
|
224
|
+
}
|
|
225
|
+
export async function resolveArchiveProjectTarget(options, dependencies = {}) {
|
|
226
|
+
const archiveProjectPath = resolveArchiveProjectPath(options.repoRoot, options.projectsRoot, options.projectName);
|
|
227
|
+
let archivePathIsGitignored = false;
|
|
228
|
+
let archiveRepoRoot = options.repoRoot;
|
|
229
|
+
let primaryRepoRoot = null;
|
|
230
|
+
let primaryRepoRootAvailable = true;
|
|
231
|
+
let localOnlyWarning = null;
|
|
232
|
+
try {
|
|
233
|
+
archivePathIsGitignored = await isGitignoredArchivePath(options.repoRoot, archiveProjectPath, dependencies);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
archivePathIsGitignored = false;
|
|
237
|
+
}
|
|
238
|
+
if (archivePathIsGitignored) {
|
|
239
|
+
const primaryResolution = await resolvePrimaryRepoRootResolution(options.repoRoot, dependencies);
|
|
240
|
+
primaryRepoRoot = primaryResolution.repoRoot;
|
|
241
|
+
primaryRepoRootAvailable = primaryResolution.available;
|
|
242
|
+
if (primaryResolution.available) {
|
|
243
|
+
archiveRepoRoot = primaryResolution.repoRoot;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
localOnlyWarning = buildLocalOnlyArchiveWarning(options.projectName, archiveProjectPath, primaryRepoRoot);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const archiveBasePath = resolveCompletionArchivePath(archiveRepoRoot, archiveProjectPath);
|
|
250
|
+
const archivePath = await resolveUniqueArchivePath(archiveBasePath, {
|
|
251
|
+
dirExists: dependencies.dirExists,
|
|
252
|
+
timestamp: dependencies.timestamp,
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
archiveProjectPath,
|
|
256
|
+
archiveRepoRoot,
|
|
257
|
+
archivePath,
|
|
258
|
+
archivePathIsGitignored,
|
|
259
|
+
primaryRepoRoot,
|
|
260
|
+
primaryRepoRootAvailable,
|
|
261
|
+
localOnlyWarning,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function assertDurableArchiveProjectTarget(target) {
|
|
265
|
+
if (target.localOnlyWarning) {
|
|
266
|
+
throw new CliError(target.localOnlyWarning);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
203
269
|
async function writeArchiveSnapshotMetadata(archivePath, metadata) {
|
|
204
270
|
await writeFile(join(archivePath, ARCHIVE_SNAPSHOT_METADATA_FILENAME), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
205
271
|
}
|
|
@@ -223,10 +289,13 @@ export async function archiveProjectOnCompletion(options, dependencies = {}) {
|
|
|
223
289
|
const execFile = dependencies.execFile ?? execFileAsync;
|
|
224
290
|
const timestamp = dependencies.timestamp?.() ?? new Date().toISOString();
|
|
225
291
|
const snapshotName = buildArchiveSnapshotName(options.projectName, timestamp);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
292
|
+
const archiveTarget = await resolveArchiveProjectTarget({
|
|
293
|
+
repoRoot: options.repoRoot,
|
|
294
|
+
projectsRoot: options.projectsRoot,
|
|
295
|
+
projectName: options.projectName,
|
|
296
|
+
}, dependencies);
|
|
297
|
+
assertDurableArchiveProjectTarget(archiveTarget);
|
|
298
|
+
const archivePath = archiveTarget.archivePath;
|
|
230
299
|
await makeDir(dirname(archivePath));
|
|
231
300
|
await copyProjectDirectory(options.projectPath, archivePath);
|
|
232
301
|
await writeArchiveSnapshotMetadata(archivePath, {
|
|
@@ -310,7 +379,7 @@ export async function ensureS3ArchiveAccess(options, dependencies = {}) {
|
|
|
310
379
|
if (options.mode === 'completion') {
|
|
311
380
|
return buildCompletionWarning('Archive S3 sync is enabled via `archive.s3SyncOnComplete`, but AWS CLI was not found on PATH. Skipping S3 archive sync.');
|
|
312
381
|
}
|
|
313
|
-
throw buildSyncError('AWS CLI is required for `oat
|
|
382
|
+
throw buildSyncError('AWS CLI is required for `oat repo archive sync`, but it was not found on PATH. Install `aws` and retry.');
|
|
314
383
|
}
|
|
315
384
|
throw error;
|
|
316
385
|
}
|
|
@@ -322,6 +391,6 @@ export async function ensureS3ArchiveAccess(options, dependencies = {}) {
|
|
|
322
391
|
if (options.mode === 'completion') {
|
|
323
392
|
return buildCompletionWarning('Archive S3 sync is enabled via `archive.s3SyncOnComplete` and `archive.s3Uri`, but AWS CLI is not configured for access. Skipping S3 archive sync.');
|
|
324
393
|
}
|
|
325
|
-
throw buildSyncError('AWS CLI is required for `oat
|
|
394
|
+
throw buildSyncError('AWS CLI is required for `oat repo archive sync`, but it is not configured for access to `archive.s3Uri`. Configure AWS credentials or profile settings and retry.');
|
|
326
395
|
}
|
|
327
396
|
}
|
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import { rm } from 'node:fs/promises';
|
|
2
|
-
import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
|
|
3
|
-
import { type OatConfig } from '../../../config/oat-config.js';
|
|
4
1
|
import { Command } from 'commander';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ensureS3ArchiveAccess: typeof ensureS3ArchiveAccess;
|
|
12
|
-
buildRepoArchiveS3Uri: typeof buildRepoArchiveS3Uri;
|
|
13
|
-
buildProjectArchiveS3Uri: typeof buildProjectArchiveS3Uri;
|
|
14
|
-
resolveLocalArchiveProjectPath: typeof resolveLocalArchiveProjectPath;
|
|
15
|
-
resolvePrimaryRepoRoot: typeof resolvePrimaryRepoRoot;
|
|
16
|
-
execFile: ExecFileLike;
|
|
17
|
-
removeDirectory: typeof rm;
|
|
18
|
-
processEnv: NodeJS.ProcessEnv;
|
|
19
|
-
}
|
|
20
|
-
export declare function createProjectArchiveCommand(overrides?: Partial<ProjectArchiveCommandDependencies>): Command;
|
|
2
|
+
import { type ProjectArchivePushCommandDependencies } from './push-runner.js';
|
|
3
|
+
import { type ProjectArchiveCommandDependencies } from './sync-runner.js';
|
|
4
|
+
export type { ArchiveSyncOptions, ProjectArchiveCommandDependencies, } from './sync-runner.js';
|
|
5
|
+
export type { ArchivePushOptions, ProjectArchivePushCommandDependencies, } from './push-runner.js';
|
|
6
|
+
type ProjectArchiveDependencies = ProjectArchiveCommandDependencies & ProjectArchivePushCommandDependencies;
|
|
7
|
+
export declare function createProjectArchiveCommand(overrides?: Partial<ProjectArchiveDependencies>): Command;
|
|
21
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/index.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAIL,KAAK,qCAAqC,EAC3C,MAAM,eAAe,CAAC;AACvB,OAAO,EAIL,KAAK,iCAAiC,EACvC,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,kBAAkB,EAClB,iCAAiC,GAClC,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qCAAqC,GACtC,MAAM,eAAe,CAAC;AAEvB,KAAK,0BAA0B,GAAG,iCAAiC,GACjE,qCAAqC,CAAC;AAKxC,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CAyET"}
|
|
@@ -1,269 +1,37 @@
|
|
|
1
|
-
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
-
import { readFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { join, posix as path } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { buildCommandContext } from '../../../app/command-context.js';
|
|
6
|
-
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
7
1
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
8
|
-
import { readOatConfig } from '../../../config/oat-config.js';
|
|
9
|
-
import { CliError } from '../../../errors/cli-error.js';
|
|
10
|
-
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
11
2
|
import { Command } from 'commander';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Resolve effective AWS profile/region for an archive sync invocation.
|
|
16
|
-
*
|
|
17
|
-
* Precedence (highest first): `--profile`/`--region` flag > `archive.awsProfile`/`archive.awsRegion`
|
|
18
|
-
* config > parent shell `AWS_PROFILE`/`AWS_REGION` env. Repo config is treated
|
|
19
|
-
* as deliberate, OAT-archive-scoped intent and wins over ambient shell state —
|
|
20
|
-
* users who explicitly set `archive.awsProfile` for the repo don't have to
|
|
21
|
-
* remember to unset `AWS_PROFILE` in every shell. Flag still beats config for
|
|
22
|
-
* one-off overrides.
|
|
23
|
-
*
|
|
24
|
-
* The returned `env` is suitable for passing into every `aws` spawn in this
|
|
25
|
-
* command. The returned `awsProfile`/`awsRegion` are also forwarded to
|
|
26
|
-
* `ensureS3ArchiveAccess` so its preflight `aws sts get-caller-identity` runs
|
|
27
|
-
* against the same identity.
|
|
28
|
-
*/
|
|
29
|
-
function resolveSyncAwsEnv(processEnv, options, config) {
|
|
30
|
-
const flagProfile = typeof options.profile === 'string' && options.profile.trim().length > 0
|
|
31
|
-
? options.profile.trim()
|
|
32
|
-
: undefined;
|
|
33
|
-
const flagRegion = typeof options.region === 'string' && options.region.trim().length > 0
|
|
34
|
-
? options.region.trim()
|
|
35
|
-
: undefined;
|
|
36
|
-
const configProfile = config.archive?.awsProfile;
|
|
37
|
-
const configRegion = config.archive?.awsRegion;
|
|
38
|
-
// Flag beats config; either is forwarded to buildAwsEnv, which clobbers the
|
|
39
|
-
// parent env entry with the resolved value. When neither flag nor config is
|
|
40
|
-
// set, awsProfile/awsRegion stay undefined and the parent env passes through
|
|
41
|
-
// untouched — letting the AWS CLI's own resolution chain take over.
|
|
42
|
-
const awsProfile = flagProfile ?? configProfile ?? undefined;
|
|
43
|
-
const awsRegion = flagRegion ?? configRegion ?? undefined;
|
|
44
|
-
const env = buildAwsEnv(processEnv, {
|
|
45
|
-
awsProfile,
|
|
46
|
-
awsRegion,
|
|
47
|
-
});
|
|
48
|
-
return { env, awsProfile, awsRegion };
|
|
49
|
-
}
|
|
50
|
-
function defaultDependencies() {
|
|
51
|
-
return {
|
|
52
|
-
buildCommandContext,
|
|
53
|
-
resolveProjectRoot,
|
|
54
|
-
readOatConfig,
|
|
55
|
-
resolveProjectsRoot,
|
|
56
|
-
ensureS3ArchiveAccess,
|
|
57
|
-
buildRepoArchiveS3Uri,
|
|
58
|
-
buildProjectArchiveS3Uri,
|
|
59
|
-
resolveLocalArchiveProjectPath,
|
|
60
|
-
resolvePrimaryRepoRoot,
|
|
61
|
-
execFile: execFileAsync,
|
|
62
|
-
removeDirectory: rm,
|
|
63
|
-
processEnv: process.env,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function resolveLocalArchiveRoot(projectsRoot) {
|
|
67
|
-
return path.join(path.dirname(projectsRoot.replace(/\/+$/, '')), 'archived');
|
|
68
|
-
}
|
|
69
|
-
function buildArchiveSyncArgs(source, target, options) {
|
|
70
|
-
const args = ['s3', 'sync', source, target];
|
|
71
|
-
for (const pattern of S3_ARCHIVE_SYNC_EXCLUDES) {
|
|
72
|
-
args.push('--exclude', pattern);
|
|
73
|
-
}
|
|
74
|
-
if (options.dryRun) {
|
|
75
|
-
args.push('--dryrun');
|
|
76
|
-
}
|
|
77
|
-
return args;
|
|
78
|
-
}
|
|
79
|
-
async function runArchiveSync(repoRoot, source, target, options, awsEnv, dependencies) {
|
|
80
|
-
await dependencies.execFile('aws', buildArchiveSyncArgs(source, target, options), {
|
|
81
|
-
cwd: repoRoot,
|
|
82
|
-
env: awsEnv,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
async function listArchiveSnapshots(repoRoot, projectsRoot, s3Uri, awsEnv, dependencies) {
|
|
86
|
-
const remoteRepoRoot = await dependencies.resolvePrimaryRepoRoot(repoRoot, {
|
|
87
|
-
gitExecFile: dependencies.execFile,
|
|
88
|
-
env: awsEnv,
|
|
89
|
-
});
|
|
90
|
-
const repoPrefix = `${dependencies.buildRepoArchiveS3Uri(s3Uri, remoteRepoRoot)}/`;
|
|
91
|
-
const { stdout } = await dependencies.execFile('aws', ['s3', 'ls', repoPrefix], {
|
|
92
|
-
cwd: repoRoot,
|
|
93
|
-
env: awsEnv,
|
|
94
|
-
});
|
|
95
|
-
return stdout
|
|
96
|
-
.split('\n')
|
|
97
|
-
.map((line) => line.match(/PRE\s+(.+?)\/?\s*$/)?.[1] ?? null)
|
|
98
|
-
.filter((snapshotName) => Boolean(snapshotName))
|
|
99
|
-
.map((snapshotName) => {
|
|
100
|
-
const parsed = parseArchiveSnapshotName(snapshotName);
|
|
101
|
-
return {
|
|
102
|
-
...parsed,
|
|
103
|
-
source: dependencies.buildProjectArchiveS3Uri(s3Uri, remoteRepoRoot, snapshotName),
|
|
104
|
-
target: dependencies.resolveLocalArchiveProjectPath(projectsRoot, parsed.projectName),
|
|
105
|
-
};
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
function compareSnapshotEntries(left, right) {
|
|
109
|
-
if (left.dateStamp && right.dateStamp && left.dateStamp !== right.dateStamp) {
|
|
110
|
-
return left.dateStamp.localeCompare(right.dateStamp);
|
|
111
|
-
}
|
|
112
|
-
if (left.dateStamp && !right.dateStamp) {
|
|
113
|
-
return 1;
|
|
114
|
-
}
|
|
115
|
-
if (!left.dateStamp && right.dateStamp) {
|
|
116
|
-
return -1;
|
|
117
|
-
}
|
|
118
|
-
return left.snapshotName.localeCompare(right.snapshotName);
|
|
119
|
-
}
|
|
120
|
-
function selectLatestSnapshots(snapshots) {
|
|
121
|
-
const latestByProject = new Map();
|
|
122
|
-
for (const snapshot of snapshots) {
|
|
123
|
-
const current = latestByProject.get(snapshot.projectName);
|
|
124
|
-
if (!current || compareSnapshotEntries(snapshot, current) > 0) {
|
|
125
|
-
latestByProject.set(snapshot.projectName, snapshot);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return [...latestByProject.values()];
|
|
129
|
-
}
|
|
130
|
-
async function readLocalSnapshotName(repoRoot, target) {
|
|
131
|
-
try {
|
|
132
|
-
const content = await readFile(join(repoRoot, target, ARCHIVE_SNAPSHOT_METADATA_FILENAME), 'utf8');
|
|
133
|
-
const parsed = JSON.parse(content);
|
|
134
|
-
return typeof parsed.snapshotName === 'string' ? parsed.snapshotName : null;
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async function syncArchiveSnapshot(repoRoot, snapshot, options, awsEnv, dependencies) {
|
|
141
|
-
const currentSnapshotName = await readLocalSnapshotName(repoRoot, snapshot.target);
|
|
142
|
-
if (!options.force && currentSnapshotName === snapshot.snapshotName) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
if (!options.dryRun) {
|
|
146
|
-
await dependencies.removeDirectory(join(repoRoot, snapshot.target), {
|
|
147
|
-
recursive: true,
|
|
148
|
-
force: true,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
await runArchiveSync(repoRoot, snapshot.source, snapshot.target, options, awsEnv, dependencies);
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
3
|
+
import { defaultProjectArchivePushCommandDependencies, runArchivePushCommand, } from './push-runner.js';
|
|
4
|
+
import { defaultProjectArchiveCommandDependencies, runArchiveSyncCommand, } from './sync-runner.js';
|
|
5
|
+
const PROJECT_ARCHIVE_SYNC_DEPRECATION_NOTICE = 'oat project archive sync is deprecated; use oat repo archive sync';
|
|
154
6
|
export function createProjectArchiveCommand(overrides = {}) {
|
|
155
7
|
const dependencies = {
|
|
156
|
-
...
|
|
8
|
+
...defaultProjectArchiveCommandDependencies(),
|
|
9
|
+
...defaultProjectArchivePushCommandDependencies(),
|
|
157
10
|
...overrides,
|
|
158
11
|
};
|
|
159
12
|
return new Command('archive')
|
|
160
13
|
.description('Manage archived project data')
|
|
14
|
+
.argument('[project-path]', 'Project path to archive')
|
|
15
|
+
.option('--dry-run', 'Preview archive without moving files or syncing S3')
|
|
16
|
+
.addHelpText('afterAll', '\nPull archived project data with `oat repo archive sync [project-name]`; `oat project archive sync` is deprecated.')
|
|
17
|
+
.action(async (projectPath, options, command) => {
|
|
18
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
19
|
+
await runArchivePushCommand(dependencies, projectPath, options, context);
|
|
20
|
+
})
|
|
161
21
|
.addCommand(new Command('sync')
|
|
162
|
-
.description('Sync archived project data from S3 into the local archive')
|
|
22
|
+
.description('[deprecated] Sync archived project data from S3 into the local archive')
|
|
163
23
|
.argument('[project-name]', 'Archived project name to sync')
|
|
164
24
|
.option('--dry-run', 'Preview archive sync without downloading')
|
|
165
25
|
.option('--force', 'Replace the named local archive before syncing it from S3')
|
|
166
26
|
.option('--profile <profile>', 'AWS profile override for this sync')
|
|
167
27
|
.option('--region <region>', 'AWS region override for this sync')
|
|
168
28
|
.action(async (projectName, options, command) => {
|
|
29
|
+
process.stderr.write(`${PROJECT_ARCHIVE_SYNC_DEPRECATION_NOTICE}\n`);
|
|
169
30
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const config = await dependencies.readOatConfig(repoRoot);
|
|
176
|
-
const s3Uri = config.archive?.s3Uri;
|
|
177
|
-
if (!s3Uri) {
|
|
178
|
-
throw new CliError('Archive sync requires `archive.s3Uri` to be configured. Set it with `oat config set archive.s3Uri <s3://...>` and retry.');
|
|
179
|
-
}
|
|
180
|
-
const { env: awsEnv, awsProfile, awsRegion, } = resolveSyncAwsEnv(dependencies.processEnv, options, config);
|
|
181
|
-
await dependencies.ensureS3ArchiveAccess({
|
|
182
|
-
mode: 'sync',
|
|
183
|
-
s3Uri,
|
|
184
|
-
syncOnComplete: config.archive?.s3SyncOnComplete ?? false,
|
|
185
|
-
awsProfile,
|
|
186
|
-
awsRegion,
|
|
187
|
-
}, { env: awsEnv });
|
|
188
|
-
const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
|
|
189
|
-
const snapshots = await listArchiveSnapshots(repoRoot, projectsRoot, s3Uri, awsEnv, dependencies);
|
|
190
|
-
const targets = projectName
|
|
191
|
-
? snapshots.filter((snapshot) => snapshot.projectName === projectName ||
|
|
192
|
-
snapshot.snapshotName === projectName)
|
|
193
|
-
: selectLatestSnapshots(snapshots);
|
|
194
|
-
if (projectName && targets.length === 0) {
|
|
195
|
-
throw new CliError(`No archived snapshot found in S3 for project \`${projectName}\`.`);
|
|
196
|
-
}
|
|
197
|
-
const latestTarget = projectName
|
|
198
|
-
? targets.reduce((latest, snapshot) => {
|
|
199
|
-
if (!latest) {
|
|
200
|
-
return snapshot;
|
|
201
|
-
}
|
|
202
|
-
return compareSnapshotEntries(snapshot, latest) > 0
|
|
203
|
-
? snapshot
|
|
204
|
-
: latest;
|
|
205
|
-
}, null)
|
|
206
|
-
: null;
|
|
207
|
-
const snapshotsToSync = projectName
|
|
208
|
-
? latestTarget
|
|
209
|
-
? [latestTarget]
|
|
210
|
-
: []
|
|
211
|
-
: targets;
|
|
212
|
-
const appliedTargets = [];
|
|
213
|
-
const appliedSources = [];
|
|
214
|
-
for (const snapshot of snapshotsToSync) {
|
|
215
|
-
if (!snapshot) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const synced = await syncArchiveSnapshot(repoRoot, snapshot, options, awsEnv, dependencies);
|
|
219
|
-
if (synced) {
|
|
220
|
-
appliedTargets.push(snapshot.target);
|
|
221
|
-
appliedSources.push(snapshot.source);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const subject = projectName
|
|
225
|
-
? `archived project \`${projectName}\``
|
|
226
|
-
: 'archived projects';
|
|
227
|
-
const targetSummary = appliedTargets.length > 0
|
|
228
|
-
? appliedTargets.join(', ')
|
|
229
|
-
: resolveLocalArchiveRoot(projectsRoot);
|
|
230
|
-
const sourceSummary = appliedSources.length > 0
|
|
231
|
-
? appliedSources.join(', ')
|
|
232
|
-
: dependencies.buildRepoArchiveS3Uri(s3Uri, await dependencies.resolvePrimaryRepoRoot(repoRoot, {
|
|
233
|
-
gitExecFile: dependencies.execFile,
|
|
234
|
-
env: awsEnv,
|
|
235
|
-
}));
|
|
236
|
-
if (context.json) {
|
|
237
|
-
context.logger.json({
|
|
238
|
-
status: 'ok',
|
|
239
|
-
mode: options.dryRun ? 'dry-run' : 'apply',
|
|
240
|
-
projectName: projectName ?? null,
|
|
241
|
-
sources: appliedSources,
|
|
242
|
-
targets: appliedTargets,
|
|
243
|
-
skipped: appliedTargets.length === 0,
|
|
244
|
-
force: options.force ?? false,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
else if (options.dryRun) {
|
|
248
|
-
context.logger.info(`Dry-run: would sync ${subject} from ${sourceSummary} to ${targetSummary}.`);
|
|
249
|
-
}
|
|
250
|
-
else if (appliedTargets.length === 0) {
|
|
251
|
-
context.logger.info(`Skipped ${subject}; local archive is already using the latest remote snapshot.`);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
context.logger.info(`Synced ${subject} from ${sourceSummary} to ${targetSummary}.`);
|
|
255
|
-
}
|
|
256
|
-
process.exitCode = 0;
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
-
if (context.json) {
|
|
261
|
-
context.logger.json({ status: 'error', message });
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
context.logger.error(message);
|
|
265
|
-
}
|
|
266
|
-
process.exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
267
|
-
}
|
|
31
|
+
const syncOptions = {
|
|
32
|
+
...options,
|
|
33
|
+
dryRun: options.dryRun ?? context.dryRun,
|
|
34
|
+
};
|
|
35
|
+
await runArchiveSyncCommand(dependencies, projectName, syncOptions, context, 'oat project archive sync');
|
|
268
36
|
}));
|
|
269
37
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
|
|
2
|
+
import { type OatConfig, type OatLocalConfig } from '../../../config/oat-config.js';
|
|
3
|
+
import { resolveArchiveProjectTarget, resolvePrimaryRepoRoot, type ArchiveProjectOnCompletionOptions, type ArchiveProjectOnCompletionResult } from './archive-utils.js';
|
|
4
|
+
export interface ArchivePushOptions {
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectArchivePushCommandDependencies {
|
|
8
|
+
buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
|
|
9
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
10
|
+
readOatConfig: (repoRoot: string) => Promise<OatConfig>;
|
|
11
|
+
readOatLocalConfig: (repoRoot: string) => Promise<OatLocalConfig>;
|
|
12
|
+
resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
|
|
13
|
+
resolvePrimaryRepoRoot: typeof resolvePrimaryRepoRoot;
|
|
14
|
+
resolveArchiveProjectTarget: typeof resolveArchiveProjectTarget;
|
|
15
|
+
archiveProjectOnCompletion: (options: ArchiveProjectOnCompletionOptions) => Promise<ArchiveProjectOnCompletionResult>;
|
|
16
|
+
processEnv: NodeJS.ProcessEnv;
|
|
17
|
+
timestamp: () => string;
|
|
18
|
+
}
|
|
19
|
+
export declare function defaultProjectArchivePushCommandDependencies(): ProjectArchivePushCommandDependencies;
|
|
20
|
+
export declare function runArchivePushCommand(dependencies: ProjectArchivePushCommandDependencies, projectPathArg: string | undefined, options: ArchivePushOptions, context: CommandContext): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=push-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-runner.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/push-runner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAKL,2BAA2B,EAC3B,sBAAsB,EAEtB,KAAK,iCAAiC,EACtC,KAAK,gCAAgC,EACtC,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qCAAqC;IACpD,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,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,2BAA2B,EAAE,OAAO,2BAA2B,CAAC;IAChE,0BAA0B,EAAE,CAC1B,OAAO,EAAE,iCAAiC,KACvC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,MAAM,MAAM,CAAC;CACzB;AAkBD,wBAAgB,4CAA4C,IAAI,qCAAqC,CAapG;AA2ID,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,qCAAqC,EACnD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAiEf"}
|