@open-agent-toolkit/cli 0.1.19 → 0.1.20

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.
Files changed (32) hide show
  1. package/README.md +2 -1
  2. package/assets/docs/cli-utilities/config-and-local-state.md +1 -1
  3. package/assets/docs/cli-utilities/configuration.md +5 -5
  4. package/assets/docs/reference/cli-reference.md +7 -5
  5. package/assets/docs/reference/file-locations.md +1 -1
  6. package/assets/docs/reference/oat-directory-structure.md +3 -3
  7. package/assets/docs/workflows/projects/index.md +1 -1
  8. package/assets/docs/workflows/projects/lifecycle.md +1 -1
  9. package/assets/docs/workflows/projects/repo-analysis.md +7 -4
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-project-complete/SKILL.md +16 -152
  12. package/assets/skills/oat-wrap-up/SKILL.md +9 -9
  13. package/assets/skills/oat-wrap-up/references/automation-recipes.md +5 -5
  14. package/dist/commands/config/index.js +2 -2
  15. package/dist/commands/project/archive/archive-utils.d.ts +19 -0
  16. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  17. package/dist/commands/project/archive/archive-utils.js +94 -25
  18. package/dist/commands/project/archive/index.d.ts +6 -19
  19. package/dist/commands/project/archive/index.d.ts.map +1 -1
  20. package/dist/commands/project/archive/index.js +19 -251
  21. package/dist/commands/project/archive/push-runner.d.ts +21 -0
  22. package/dist/commands/project/archive/push-runner.d.ts.map +1 -0
  23. package/dist/commands/project/archive/push-runner.js +151 -0
  24. package/dist/commands/project/archive/sync-runner.d.ts +47 -0
  25. package/dist/commands/project/archive/sync-runner.d.ts.map +1 -0
  26. package/dist/commands/project/archive/sync-runner.js +232 -0
  27. package/dist/commands/repo/archive/index.d.ts +4 -0
  28. package/dist/commands/repo/archive/index.d.ts.map +1 -0
  29. package/dist/commands/repo/archive/index.js +22 -0
  30. package/dist/commands/repo/index.d.ts.map +1 -1
  31. package/dist/commands/repo/index.js +2 -0
  32. package/package.json +2 -2
@@ -0,0 +1,151 @@
1
+ import { basename, isAbsolute, join } from 'node:path';
2
+ import { buildCommandContext } from '../../../app/command-context.js';
3
+ import { resolveProjectsRoot } from '../../shared/oat-paths.js';
4
+ import { readOatConfig, readOatLocalConfig, } from '../../../config/oat-config.js';
5
+ import { CliError } from '../../../errors/cli-error.js';
6
+ import { resolveProjectRoot } from '../../../fs/paths.js';
7
+ import { archiveProjectOnCompletion, assertDurableArchiveProjectTarget, buildArchiveSnapshotName, buildProjectArchiveS3Uri, resolveArchiveProjectTarget, resolvePrimaryRepoRoot, } from './archive-utils.js';
8
+ export function defaultProjectArchivePushCommandDependencies() {
9
+ return {
10
+ buildCommandContext,
11
+ resolveProjectRoot,
12
+ readOatConfig,
13
+ readOatLocalConfig,
14
+ resolveProjectsRoot,
15
+ resolvePrimaryRepoRoot,
16
+ resolveArchiveProjectTarget,
17
+ archiveProjectOnCompletion,
18
+ processEnv: process.env,
19
+ timestamp: () => new Date().toISOString(),
20
+ };
21
+ }
22
+ function resolveRepoAbsolutePath(repoRoot, targetPath) {
23
+ return isAbsolute(targetPath) ? targetPath : join(repoRoot, targetPath);
24
+ }
25
+ async function resolveArchiveTarget(dependencies, repoRoot, projectPathArg) {
26
+ const rawProjectPath = projectPathArg?.trim();
27
+ const projectPath = rawProjectPath && rawProjectPath.length > 0
28
+ ? rawProjectPath
29
+ : (await dependencies.readOatLocalConfig(repoRoot)).activeProject?.trim();
30
+ if (!projectPath) {
31
+ throw new CliError('No project path provided and no active project is configured. Pass a project path or set `activeProject`.');
32
+ }
33
+ const absoluteProjectPath = resolveRepoAbsolutePath(repoRoot, projectPath);
34
+ const projectName = basename(absoluteProjectPath);
35
+ if (!projectName) {
36
+ throw new CliError(`Unable to determine project name from \`${projectPath}\`.`);
37
+ }
38
+ return {
39
+ projectName,
40
+ projectPath: absoluteProjectPath,
41
+ };
42
+ }
43
+ function buildArchiveOptions(repoRoot, config, projectsRoot, target) {
44
+ return {
45
+ repoRoot,
46
+ projectPath: target.projectPath,
47
+ projectName: target.projectName,
48
+ projectsRoot,
49
+ s3Uri: config.archive?.s3Uri,
50
+ s3SyncOnComplete: config.archive?.s3SyncOnComplete ?? false,
51
+ summaryExportPath: config.archive?.summaryExportPath,
52
+ awsProfile: config.archive?.awsProfile,
53
+ awsRegion: config.archive?.awsRegion,
54
+ };
55
+ }
56
+ async function buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget) {
57
+ const snapshotName = buildArchiveSnapshotName(target.projectName, dependencies.timestamp());
58
+ const remoteRepoRoot = await dependencies.resolvePrimaryRepoRoot(repoRoot);
59
+ const s3Path = config.archive?.s3Uri && config.archive.s3SyncOnComplete === true
60
+ ? buildProjectArchiveS3Uri(config.archive.s3Uri, remoteRepoRoot, snapshotName)
61
+ : null;
62
+ const summaryExportFile = config.archive?.summaryExportPath
63
+ ? join(repoRoot, config.archive.summaryExportPath, `${snapshotName}.md`)
64
+ : null;
65
+ return {
66
+ status: 'ok',
67
+ mode: 'dry-run',
68
+ projectName: target.projectName,
69
+ projectPath: target.projectPath,
70
+ archivePath: archiveTarget.archivePath,
71
+ s3Path,
72
+ summaryExportFile,
73
+ warnings: [],
74
+ };
75
+ }
76
+ function emitArchivePushText(report, summaryExportPath, logger) {
77
+ for (const warning of report.warnings) {
78
+ logger.warn(warning);
79
+ }
80
+ if (report.mode === 'dry-run') {
81
+ logger.info(`Dry-run: would archive project \`${report.projectName}\` from \`${report.projectPath}\` to \`${report.archivePath}\`.`);
82
+ if (report.s3Path) {
83
+ logger.info(`S3 archive: ${report.s3Path}`);
84
+ }
85
+ if (summaryExportPath) {
86
+ logger.info(`Summary export path: ${summaryExportPath}`);
87
+ }
88
+ return;
89
+ }
90
+ logger.info(`Archived project \`${report.projectName}\` to \`${report.archivePath}\`.`);
91
+ if (report.s3Path) {
92
+ logger.info(`S3 archive: ${report.s3Path}`);
93
+ }
94
+ if (report.summaryExportFile) {
95
+ logger.info(`Summary export: ${report.summaryExportFile}`);
96
+ }
97
+ }
98
+ function emitArchivePushReport(report, summaryExportPath, context) {
99
+ if (context.json) {
100
+ context.logger.json(report);
101
+ return;
102
+ }
103
+ emitArchivePushText(report, summaryExportPath, context.logger);
104
+ }
105
+ export async function runArchivePushCommand(dependencies, projectPathArg, options, context) {
106
+ try {
107
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
108
+ const config = await dependencies.readOatConfig(repoRoot);
109
+ const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
110
+ const target = await resolveArchiveTarget(dependencies, repoRoot, projectPathArg);
111
+ const archiveTarget = await dependencies.resolveArchiveProjectTarget({
112
+ repoRoot,
113
+ projectsRoot,
114
+ projectName: target.projectName,
115
+ }, {
116
+ env: dependencies.processEnv,
117
+ timestamp: dependencies.timestamp,
118
+ });
119
+ assertDurableArchiveProjectTarget(archiveTarget);
120
+ const dryRun = options.dryRun === true || context.dryRun;
121
+ if (dryRun) {
122
+ const report = await buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget);
123
+ emitArchivePushReport(report, config.archive?.summaryExportPath, context);
124
+ process.exitCode = 0;
125
+ return;
126
+ }
127
+ const result = await dependencies.archiveProjectOnCompletion(buildArchiveOptions(repoRoot, config, projectsRoot, target));
128
+ const report = {
129
+ status: 'ok',
130
+ mode: 'apply',
131
+ projectName: target.projectName,
132
+ projectPath: target.projectPath,
133
+ archivePath: result.archivePath,
134
+ s3Path: result.s3Path,
135
+ summaryExportFile: result.summaryExportFile,
136
+ warnings: result.warnings,
137
+ };
138
+ emitArchivePushReport(report, config.archive?.summaryExportPath, context);
139
+ process.exitCode = 0;
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ if (context.json) {
144
+ context.logger.json({ status: 'error', message });
145
+ }
146
+ else {
147
+ context.logger.error(message);
148
+ }
149
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
150
+ }
151
+ }
@@ -0,0 +1,47 @@
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
+ import { buildProjectArchiveS3Uri, buildRepoArchiveS3Uri, ensureS3ArchiveAccess, type ExecFileLike, resolveLocalArchiveProjectPath, resolvePrimaryRepoRoot } from './archive-utils.js';
5
+ export interface ArchiveSyncOptions {
6
+ dryRun?: boolean;
7
+ force?: boolean;
8
+ profile?: string;
9
+ region?: string;
10
+ }
11
+ export interface ProjectArchiveCommandDependencies {
12
+ buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
13
+ resolveProjectRoot: (cwd: string) => Promise<string>;
14
+ readOatConfig: (repoRoot: string) => Promise<OatConfig>;
15
+ resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
16
+ ensureS3ArchiveAccess: typeof ensureS3ArchiveAccess;
17
+ buildRepoArchiveS3Uri: typeof buildRepoArchiveS3Uri;
18
+ buildProjectArchiveS3Uri: typeof buildProjectArchiveS3Uri;
19
+ resolveLocalArchiveProjectPath: typeof resolveLocalArchiveProjectPath;
20
+ resolvePrimaryRepoRoot: typeof resolvePrimaryRepoRoot;
21
+ execFile: ExecFileLike;
22
+ removeDirectory: typeof rm;
23
+ processEnv: NodeJS.ProcessEnv;
24
+ }
25
+ export declare function defaultProjectArchiveCommandDependencies(): ProjectArchiveCommandDependencies;
26
+ export declare function resolveLocalArchiveRoot(projectsRoot: string): string;
27
+ export declare function resolveSyncAwsEnv(processEnv: NodeJS.ProcessEnv, options: ArchiveSyncOptions, config: OatConfig): {
28
+ env: NodeJS.ProcessEnv;
29
+ awsProfile: string | undefined;
30
+ awsRegion: string | undefined;
31
+ };
32
+ export declare function buildArchiveSyncArgs(source: string, target: string, options: ArchiveSyncOptions): string[];
33
+ export declare function runArchiveSync(repoRoot: string, source: string, target: string, options: ArchiveSyncOptions, awsEnv: NodeJS.ProcessEnv, dependencies: ProjectArchiveCommandDependencies): Promise<void>;
34
+ export interface ArchiveSnapshotEntry {
35
+ dateStamp: string | null;
36
+ projectName: string;
37
+ snapshotName: string;
38
+ source: string;
39
+ target: string;
40
+ }
41
+ export declare function listArchiveSnapshots(repoRoot: string, projectsRoot: string, s3Uri: string, awsEnv: NodeJS.ProcessEnv, dependencies: ProjectArchiveCommandDependencies): Promise<ArchiveSnapshotEntry[]>;
42
+ export declare function compareSnapshotEntries(left: ArchiveSnapshotEntry, right: ArchiveSnapshotEntry): number;
43
+ export declare function selectLatestSnapshots(snapshots: ArchiveSnapshotEntry[]): ArchiveSnapshotEntry[];
44
+ export declare function readLocalSnapshotName(repoRoot: string, target: string): Promise<string | null>;
45
+ export declare function syncArchiveSnapshot(repoRoot: string, snapshot: ArchiveSnapshotEntry, options: ArchiveSyncOptions, awsEnv: NodeJS.ProcessEnv, dependencies: ProjectArchiveCommandDependencies): Promise<boolean>;
46
+ export declare function runArchiveSyncCommand(dependencies: ProjectArchiveCommandDependencies, projectName: string | undefined, options: ArchiveSyncOptions, context: CommandContext, commandLabel?: string): Promise<void>;
47
+ //# sourceMappingURL=sync-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-runner.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/sync-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,232 @@
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
+ import { readOatConfig } from '../../../config/oat-config.js';
8
+ import { CliError } from '../../../errors/cli-error.js';
9
+ import { resolveProjectRoot } from '../../../fs/paths.js';
10
+ import { ARCHIVE_SNAPSHOT_METADATA_FILENAME, S3_ARCHIVE_SYNC_EXCLUDES, buildAwsEnv, buildProjectArchiveS3Uri, buildRepoArchiveS3Uri, ensureS3ArchiveAccess, parseArchiveSnapshotName, resolveLocalArchiveProjectPath, resolvePrimaryRepoRoot, } from './archive-utils.js';
11
+ const execFileAsync = promisify(execFileCallback);
12
+ export function defaultProjectArchiveCommandDependencies() {
13
+ return {
14
+ buildCommandContext,
15
+ resolveProjectRoot,
16
+ readOatConfig,
17
+ resolveProjectsRoot,
18
+ ensureS3ArchiveAccess,
19
+ buildRepoArchiveS3Uri,
20
+ buildProjectArchiveS3Uri,
21
+ resolveLocalArchiveProjectPath,
22
+ resolvePrimaryRepoRoot,
23
+ execFile: execFileAsync,
24
+ removeDirectory: rm,
25
+ processEnv: process.env,
26
+ };
27
+ }
28
+ export function resolveLocalArchiveRoot(projectsRoot) {
29
+ return path.join(path.dirname(projectsRoot.replace(/\/+$/, '')), 'archived');
30
+ }
31
+ export function resolveSyncAwsEnv(processEnv, options, config) {
32
+ const flagProfile = typeof options.profile === 'string' && options.profile.trim().length > 0
33
+ ? options.profile.trim()
34
+ : undefined;
35
+ const flagRegion = typeof options.region === 'string' && options.region.trim().length > 0
36
+ ? options.region.trim()
37
+ : undefined;
38
+ const configProfile = config.archive?.awsProfile;
39
+ const configRegion = config.archive?.awsRegion;
40
+ const awsProfile = flagProfile ?? configProfile ?? undefined;
41
+ const awsRegion = flagRegion ?? configRegion ?? undefined;
42
+ const env = buildAwsEnv(processEnv, {
43
+ awsProfile,
44
+ awsRegion,
45
+ });
46
+ return { env, awsProfile, awsRegion };
47
+ }
48
+ export function buildArchiveSyncArgs(source, target, options) {
49
+ const args = ['s3', 'sync', source, target];
50
+ for (const pattern of S3_ARCHIVE_SYNC_EXCLUDES) {
51
+ args.push('--exclude', pattern);
52
+ }
53
+ if (options.dryRun) {
54
+ args.push('--dryrun');
55
+ }
56
+ return args;
57
+ }
58
+ export async function runArchiveSync(repoRoot, source, target, options, awsEnv, dependencies) {
59
+ await dependencies.execFile('aws', buildArchiveSyncArgs(source, target, options), {
60
+ cwd: repoRoot,
61
+ env: awsEnv,
62
+ });
63
+ }
64
+ export async function listArchiveSnapshots(repoRoot, projectsRoot, s3Uri, awsEnv, dependencies) {
65
+ const remoteRepoRoot = await dependencies.resolvePrimaryRepoRoot(repoRoot, {
66
+ gitExecFile: dependencies.execFile,
67
+ env: awsEnv,
68
+ });
69
+ const repoPrefix = `${dependencies.buildRepoArchiveS3Uri(s3Uri, remoteRepoRoot)}/`;
70
+ const { stdout } = await dependencies.execFile('aws', ['s3', 'ls', repoPrefix], {
71
+ cwd: repoRoot,
72
+ env: awsEnv,
73
+ });
74
+ return stdout
75
+ .split('\n')
76
+ .map((line) => line.match(/PRE\s+(.+?)\/?\s*$/)?.[1] ?? null)
77
+ .filter((snapshotName) => Boolean(snapshotName))
78
+ .map((snapshotName) => {
79
+ const parsed = parseArchiveSnapshotName(snapshotName);
80
+ return {
81
+ ...parsed,
82
+ source: dependencies.buildProjectArchiveS3Uri(s3Uri, remoteRepoRoot, snapshotName),
83
+ target: dependencies.resolveLocalArchiveProjectPath(projectsRoot, parsed.projectName),
84
+ };
85
+ });
86
+ }
87
+ export function compareSnapshotEntries(left, right) {
88
+ if (left.dateStamp && right.dateStamp && left.dateStamp !== right.dateStamp) {
89
+ return left.dateStamp.localeCompare(right.dateStamp);
90
+ }
91
+ if (left.dateStamp && !right.dateStamp) {
92
+ return 1;
93
+ }
94
+ if (!left.dateStamp && right.dateStamp) {
95
+ return -1;
96
+ }
97
+ return left.snapshotName.localeCompare(right.snapshotName);
98
+ }
99
+ export function selectLatestSnapshots(snapshots) {
100
+ const latestByProject = new Map();
101
+ for (const snapshot of snapshots) {
102
+ const current = latestByProject.get(snapshot.projectName);
103
+ if (!current || compareSnapshotEntries(snapshot, current) > 0) {
104
+ latestByProject.set(snapshot.projectName, snapshot);
105
+ }
106
+ }
107
+ return [...latestByProject.values()];
108
+ }
109
+ export async function readLocalSnapshotName(repoRoot, target) {
110
+ try {
111
+ const content = await readFile(join(repoRoot, target, ARCHIVE_SNAPSHOT_METADATA_FILENAME), 'utf8');
112
+ const parsed = JSON.parse(content);
113
+ return typeof parsed.snapshotName === 'string' ? parsed.snapshotName : null;
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ export async function syncArchiveSnapshot(repoRoot, snapshot, options, awsEnv, dependencies) {
120
+ const currentSnapshotName = await readLocalSnapshotName(repoRoot, snapshot.target);
121
+ if (!options.force && currentSnapshotName === snapshot.snapshotName) {
122
+ return false;
123
+ }
124
+ if (!options.dryRun) {
125
+ await dependencies.removeDirectory(join(repoRoot, snapshot.target), {
126
+ recursive: true,
127
+ force: true,
128
+ });
129
+ }
130
+ await runArchiveSync(repoRoot, snapshot.source, snapshot.target, options, awsEnv, dependencies);
131
+ return true;
132
+ }
133
+ export async function runArchiveSyncCommand(dependencies, projectName, options, context, commandLabel = 'oat repo archive sync') {
134
+ try {
135
+ if (options.force && !projectName) {
136
+ throw new CliError(`\`--force\` requires a project name for \`${commandLabel}\`.`);
137
+ }
138
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
139
+ const config = await dependencies.readOatConfig(repoRoot);
140
+ const s3Uri = config.archive?.s3Uri;
141
+ if (!s3Uri) {
142
+ throw new CliError('Archive sync requires `archive.s3Uri` to be configured. Set it with `oat config set archive.s3Uri <s3://...>` and retry.');
143
+ }
144
+ const { env: awsEnv, awsProfile, awsRegion, } = resolveSyncAwsEnv(dependencies.processEnv, options, config);
145
+ await dependencies.ensureS3ArchiveAccess({
146
+ mode: 'sync',
147
+ s3Uri,
148
+ syncOnComplete: config.archive?.s3SyncOnComplete ?? false,
149
+ awsProfile,
150
+ awsRegion,
151
+ }, { env: awsEnv });
152
+ const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
153
+ const snapshots = await listArchiveSnapshots(repoRoot, projectsRoot, s3Uri, awsEnv, dependencies);
154
+ const targets = projectName
155
+ ? snapshots.filter((snapshot) => snapshot.projectName === projectName ||
156
+ snapshot.snapshotName === projectName)
157
+ : selectLatestSnapshots(snapshots);
158
+ if (projectName && targets.length === 0) {
159
+ throw new CliError(`No archived snapshot found in S3 for project \`${projectName}\`.`);
160
+ }
161
+ const latestTarget = projectName
162
+ ? targets.reduce((latest, snapshot) => {
163
+ if (!latest) {
164
+ return snapshot;
165
+ }
166
+ return compareSnapshotEntries(snapshot, latest) > 0
167
+ ? snapshot
168
+ : latest;
169
+ }, null)
170
+ : null;
171
+ const snapshotsToSync = projectName
172
+ ? latestTarget
173
+ ? [latestTarget]
174
+ : []
175
+ : targets;
176
+ const appliedTargets = [];
177
+ const appliedSources = [];
178
+ for (const snapshot of snapshotsToSync) {
179
+ if (!snapshot) {
180
+ continue;
181
+ }
182
+ const synced = await syncArchiveSnapshot(repoRoot, snapshot, options, awsEnv, dependencies);
183
+ if (synced) {
184
+ appliedTargets.push(snapshot.target);
185
+ appliedSources.push(snapshot.source);
186
+ }
187
+ }
188
+ const subject = projectName
189
+ ? `archived project \`${projectName}\``
190
+ : 'archived projects';
191
+ const targetSummary = appliedTargets.length > 0
192
+ ? appliedTargets.join(', ')
193
+ : resolveLocalArchiveRoot(projectsRoot);
194
+ const sourceSummary = appliedSources.length > 0
195
+ ? appliedSources.join(', ')
196
+ : dependencies.buildRepoArchiveS3Uri(s3Uri, await dependencies.resolvePrimaryRepoRoot(repoRoot, {
197
+ gitExecFile: dependencies.execFile,
198
+ env: awsEnv,
199
+ }));
200
+ if (context.json) {
201
+ context.logger.json({
202
+ status: 'ok',
203
+ mode: options.dryRun ? 'dry-run' : 'apply',
204
+ projectName: projectName ?? null,
205
+ sources: appliedSources,
206
+ targets: appliedTargets,
207
+ skipped: appliedTargets.length === 0,
208
+ force: options.force ?? false,
209
+ });
210
+ }
211
+ else if (options.dryRun) {
212
+ context.logger.info(`Dry-run: would sync ${subject} from ${sourceSummary} to ${targetSummary}.`);
213
+ }
214
+ else if (appliedTargets.length === 0) {
215
+ context.logger.info(`Skipped ${subject}; local archive is already using the latest remote snapshot.`);
216
+ }
217
+ else {
218
+ context.logger.info(`Synced ${subject} from ${sourceSummary} to ${targetSummary}.`);
219
+ }
220
+ process.exitCode = 0;
221
+ }
222
+ catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ if (context.json) {
225
+ context.logger.json({ status: 'error', message });
226
+ }
227
+ else {
228
+ context.logger.error(message);
229
+ }
230
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
231
+ }
232
+ }
@@ -0,0 +1,4 @@
1
+ import { type ProjectArchiveCommandDependencies } from '../../project/archive/sync-runner.js';
2
+ import { Command } from 'commander';
3
+ export declare function createRepoArchiveCommand(overrides?: Partial<ProjectArchiveCommandDependencies>): Command;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/repo/archive/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,iCAAiC,EACvC,MAAM,uCAAuC,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,iCAAiC,CAAM,GACzD,OAAO,CAyCT"}
@@ -0,0 +1,22 @@
1
+ import { defaultProjectArchiveCommandDependencies, runArchiveSyncCommand, } from '../../project/archive/sync-runner.js';
2
+ import { readGlobalOptions } from '../../shared/shared.utils.js';
3
+ import { Command } from 'commander';
4
+ export function createRepoArchiveCommand(overrides = {}) {
5
+ const dependencies = {
6
+ ...defaultProjectArchiveCommandDependencies(),
7
+ ...overrides,
8
+ };
9
+ return new Command('archive')
10
+ .description('Manage repository archive data')
11
+ .addCommand(new Command('sync')
12
+ .description('Sync archived project data from S3 into the local archive')
13
+ .argument('[project-name]', 'Archived project name to sync')
14
+ .option('--dry-run', 'Preview archive sync without downloading')
15
+ .option('--force', 'Replace the named local archive before syncing it from S3')
16
+ .option('--profile <profile>', 'AWS profile override for this sync')
17
+ .option('--region <region>', 'AWS region override for this sync')
18
+ .action(async (projectName, options, command) => {
19
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
20
+ await runArchiveSyncCommand(dependencies, projectName, options, context, 'oat repo archive sync');
21
+ }));
22
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/repo/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,wBAAgB,iBAAiB,IAAI,OAAO,CAQ3C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/repo/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,iBAAiB,IAAI,OAAO,CAS3C"}
@@ -1,7 +1,9 @@
1
1
  import { Command } from 'commander';
2
+ import { createRepoArchiveCommand } from './archive/index.js';
2
3
  import { createPrCommentsCommand } from './pr-comments/index.js';
3
4
  export function createRepoCommand() {
4
5
  const cmd = new Command('repo').description('Repository-level analysis and insight tools');
6
+ cmd.addCommand(createRepoArchiveCommand());
5
7
  cmd.addCommand(createPrCommentsCommand());
6
8
  return cmd;
7
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.1.19"
37
+ "@open-agent-toolkit/control-plane": "0.1.20"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",