@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.
Files changed (59) hide show
  1. package/README.md +2 -1
  2. package/assets/agents/oat-reviewer.md +48 -10
  3. package/assets/docs/cli-utilities/config-and-local-state.md +13 -1
  4. package/assets/docs/cli-utilities/configuration.md +24 -6
  5. package/assets/docs/docs-tooling/workflows.md +8 -2
  6. package/assets/docs/reference/cli-reference.md +13 -7
  7. package/assets/docs/reference/file-locations.md +1 -1
  8. package/assets/docs/reference/oat-directory-structure.md +3 -3
  9. package/assets/docs/workflows/projects/index.md +1 -1
  10. package/assets/docs/workflows/projects/lifecycle.md +1 -1
  11. package/assets/docs/workflows/projects/repo-analysis.md +7 -4
  12. package/assets/docs/workflows/projects/reviews.md +41 -0
  13. package/assets/public-package-versions.json +4 -4
  14. package/assets/skills/oat-agent-instructions-analyze/SKILL.md +43 -13
  15. package/assets/skills/oat-docs-analyze/SKILL.md +42 -12
  16. package/assets/skills/oat-project-complete/SKILL.md +16 -152
  17. package/assets/skills/oat-project-discover/SKILL.md +22 -4
  18. package/assets/skills/oat-project-import-plan/SKILL.md +38 -9
  19. package/assets/skills/oat-project-plan/SKILL.md +30 -7
  20. package/assets/skills/oat-project-plan-writing/SKILL.md +45 -2
  21. package/assets/skills/oat-project-progress/SKILL.md +9 -3
  22. package/assets/skills/oat-project-quick-start/SKILL.md +40 -8
  23. package/assets/skills/oat-project-review-provide/SKILL.md +24 -11
  24. package/assets/skills/oat-project-review-receive/SKILL.md +37 -17
  25. package/assets/skills/oat-wrap-up/SKILL.md +9 -9
  26. package/assets/skills/oat-wrap-up/references/automation-recipes.md +5 -5
  27. package/dist/commands/config/index.d.ts.map +1 -1
  28. package/dist/commands/config/index.js +38 -2
  29. package/dist/commands/index.d.ts.map +1 -1
  30. package/dist/commands/index.js +2 -0
  31. package/dist/commands/project/archive/archive-utils.d.ts +19 -0
  32. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  33. package/dist/commands/project/archive/archive-utils.js +94 -25
  34. package/dist/commands/project/archive/index.d.ts +6 -19
  35. package/dist/commands/project/archive/index.d.ts.map +1 -1
  36. package/dist/commands/project/archive/index.js +19 -251
  37. package/dist/commands/project/archive/push-runner.d.ts +21 -0
  38. package/dist/commands/project/archive/push-runner.d.ts.map +1 -0
  39. package/dist/commands/project/archive/push-runner.js +151 -0
  40. package/dist/commands/project/archive/sync-runner.d.ts +47 -0
  41. package/dist/commands/project/archive/sync-runner.d.ts.map +1 -0
  42. package/dist/commands/project/archive/sync-runner.js +232 -0
  43. package/dist/commands/repo/archive/index.d.ts +4 -0
  44. package/dist/commands/repo/archive/index.d.ts.map +1 -0
  45. package/dist/commands/repo/archive/index.js +22 -0
  46. package/dist/commands/repo/index.d.ts.map +1 -1
  47. package/dist/commands/repo/index.js +2 -0
  48. package/dist/commands/review/index.d.ts +3 -0
  49. package/dist/commands/review/index.d.ts.map +1 -0
  50. package/dist/commands/review/index.js +7 -0
  51. package/dist/commands/review/latest.d.ts +23 -0
  52. package/dist/commands/review/latest.d.ts.map +1 -0
  53. package/dist/commands/review/latest.js +182 -0
  54. package/dist/config/oat-config.d.ts +5 -0
  55. package/dist/config/oat-config.d.ts.map +1 -1
  56. package/dist/config/oat-config.js +12 -0
  57. package/dist/config/resolve.d.ts.map +1 -1
  58. package/dist/config/resolve.js +4 -0
  59. 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 resolveCompletionArchivePath(archiveRepoRoot, projectsRoot, projectName) {
116
- return join(archiveRepoRoot, resolveLocalArchiveProjectPath(projectsRoot, projectName));
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
- export async function resolvePrimaryRepoRoot(repoRoot, dependencies = {}) {
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 resolveArchiveRepoRoot(repoRoot, archiveProjectPath, dependencies) {
183
- try {
184
- const archivePathIsGitignored = await isGitignoredArchivePath(repoRoot, archiveProjectPath, dependencies);
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 archiveProjectPath = resolveLocalArchiveProjectPath(options.projectsRoot, options.projectName);
227
- const archiveRepoRoot = await resolveArchiveRepoRoot(options.repoRoot, archiveProjectPath, dependencies);
228
- const archiveBasePath = resolveCompletionArchivePath(archiveRepoRoot, options.projectsRoot, options.projectName);
229
- const archivePath = await resolveUniqueArchivePath(archiveBasePath, dependencies);
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 project archive sync`, but it was not found on PATH. Install `aws` and retry.');
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 project archive sync`, but it is not configured for access to `archive.s3Uri`. Configure AWS credentials or profile settings and retry.');
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 { buildProjectArchiveS3Uri, buildRepoArchiveS3Uri, ensureS3ArchiveAccess, type ExecFileLike, resolveLocalArchiveProjectPath, resolvePrimaryRepoRoot } from './archive-utils.js';
6
- export interface ProjectArchiveCommandDependencies {
7
- buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
8
- resolveProjectRoot: (cwd: string) => Promise<string>;
9
- readOatConfig: (repoRoot: string) => Promise<OatConfig>;
10
- resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
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,EAAY,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAIhD,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGhF,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,oBAAoB,CAAC;AAGnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAIL,wBAAwB,EACxB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,YAAY,EAEjB,8BAA8B,EAC9B,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AA8DzB,MAAM,WAAW,iCAAiC;IAChD,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,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,qBAAqB,EAAE,OAAO,qBAAqB,CAAC;IACpD,qBAAqB,EAAE,OAAO,qBAAqB,CAAC;IACpD,wBAAwB,EAAE,OAAO,wBAAwB,CAAC;IAC1D,8BAA8B,EAAE,OAAO,8BAA8B,CAAC;IACtE,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,QAAQ,EAAE,YAAY,CAAC;IACvB,eAAe,EAAE,OAAO,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAoMD,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,iCAAiC,CAAM,GACzD,OAAO,CA4LT"}
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 { ARCHIVE_SNAPSHOT_METADATA_FILENAME, S3_ARCHIVE_SYNC_EXCLUDES, buildAwsEnv, buildProjectArchiveS3Uri, buildRepoArchiveS3Uri, ensureS3ArchiveAccess, parseArchiveSnapshotName, resolveLocalArchiveProjectPath, resolvePrimaryRepoRoot, } from './archive-utils.js';
13
- const execFileAsync = promisify(execFileCallback);
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
- ...defaultDependencies(),
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
- try {
171
- if (options.force && !projectName) {
172
- throw new CliError('`--force` requires a project name for `oat project archive sync`.');
173
- }
174
- const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
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"}