@open-agent-toolkit/cli 0.1.18 → 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 (34) 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-agent-instructions-apply/SKILL.md +4 -2
  12. package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/frontmatter/canonical-rule.md +93 -0
  13. package/assets/skills/oat-project-complete/SKILL.md +16 -152
  14. package/assets/skills/oat-wrap-up/SKILL.md +9 -9
  15. package/assets/skills/oat-wrap-up/references/automation-recipes.md +5 -5
  16. package/dist/commands/config/index.js +2 -2
  17. package/dist/commands/project/archive/archive-utils.d.ts +19 -0
  18. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  19. package/dist/commands/project/archive/archive-utils.js +94 -25
  20. package/dist/commands/project/archive/index.d.ts +6 -19
  21. package/dist/commands/project/archive/index.d.ts.map +1 -1
  22. package/dist/commands/project/archive/index.js +19 -251
  23. package/dist/commands/project/archive/push-runner.d.ts +21 -0
  24. package/dist/commands/project/archive/push-runner.d.ts.map +1 -0
  25. package/dist/commands/project/archive/push-runner.js +151 -0
  26. package/dist/commands/project/archive/sync-runner.d.ts +47 -0
  27. package/dist/commands/project/archive/sync-runner.d.ts.map +1 -0
  28. package/dist/commands/project/archive/sync-runner.js +232 -0
  29. package/dist/commands/repo/archive/index.d.ts +4 -0
  30. package/dist/commands/repo/archive/index.d.ts.map +1 -0
  31. package/dist/commands/repo/archive/index.js +22 -0
  32. package/dist/commands/repo/index.d.ts.map +1 -1
  33. package/dist/commands/repo/index.js +2 -0
  34. package/package.json +2 -2
@@ -35,7 +35,7 @@ The generic pattern:
35
35
 
36
36
  1. Create a scheduled trigger whose prompt is `/oat-wrap-up --past-week` (or a named range that matches your cadence).
37
37
  2. Configure the trigger to run in a workspace where the OAT repository is the current working directory.
38
- 3. Ensure the workspace has `gh` authenticated and, for cross-teammate visibility, a recent run of `oat project archive sync` (this can be a pre-prompt step in the trigger).
38
+ 3. Ensure the workspace has `gh` authenticated and, for cross-teammate visibility, a recent run of `oat repo archive sync` (this can be a pre-prompt step in the trigger).
39
39
 
40
40
  If your Codex host does not expose scheduled triggers, fall through to Pattern 3.
41
41
 
@@ -48,7 +48,7 @@ When neither Claude Code's `CronCreate` nor Codex host scheduling is available,
48
48
  ```cron
49
49
  # Every Monday at 09:00 local time, generate a past-week wrap-up and commit it.
50
50
  0 9 * * 1 cd /path/to/repo && \
51
- oat project archive sync --dry-run > /dev/null && \
51
+ oat repo archive sync --dry-run > /dev/null && \
52
52
  claude -p "/oat-wrap-up --past-week" && \
53
53
  git add .oat/repo/reference/wrap-ups && \
54
54
  git -c user.name="wrapup-bot" -c user.email="wrapup-bot@example.com" commit -m "chore: weekly wrap-up $(date -u +%Y-%m-%d)" >/dev/null 2>&1 || true
@@ -57,7 +57,7 @@ When neither Claude Code's `CronCreate` nor Codex host scheduling is available,
57
57
  Notes:
58
58
 
59
59
  - Replace `claude -p ...` with your host's headless invocation (`codex -p ...`, etc.) if using a different host.
60
- - The `oat project archive sync --dry-run` call is a preflight check — it surfaces auth or config problems early. Change it to `oat project archive sync` if you want to actively pull teammates' archives before each run.
60
+ - The `oat repo archive sync --dry-run` call is a preflight check — it surfaces auth or config problems early. Change it to `oat repo archive sync` if you want to actively pull teammates' archives before each run.
61
61
  - The trailing `|| true` keeps cron quiet when there is nothing new to commit (e.g., the wrap-up matched a previous one).
62
62
 
63
63
  ### systemd timer example
@@ -71,7 +71,7 @@ Description=Generate weekly OAT wrap-up
71
71
  [Service]
72
72
  Type=oneshot
73
73
  WorkingDirectory=/path/to/repo
74
- ExecStart=/bin/sh -c 'oat project archive sync --dry-run && claude -p "/oat-wrap-up --past-week"'
74
+ ExecStart=/bin/sh -c 'oat repo archive sync --dry-run && claude -p "/oat-wrap-up --past-week"'
75
75
  User=wrapup-bot
76
76
  ```
77
77
 
@@ -96,5 +96,5 @@ Enable with `systemctl enable --now oat-wrap-up.timer`.
96
96
  - **Timezones**: The skill resolves named ranges against `today` in the local timezone of the host running the skill. Always pin the timezone at the scheduler layer (cron `TZ=`, systemd `OnCalendar=Mon 09:00 America/Los_Angeles`, or the Claude Code trigger's `timezone` field) so reports are reproducible across re-runs.
97
97
  - **Cadence vs window length**: A weekly cadence usually pairs with `--past-week`. A biweekly cadence with `--past-2-weeks`. Running a weekly cadence with `--past-2-weeks` overlaps every report — fine if you want a rolling window, confusing if you expect discrete chunks.
98
98
  - **Commit + push**: The skill itself never commits. If you want the report to land in the repo automatically, the automation wrapper owns the `git add && git commit && git push` step. Keep that wrapper out of the skill so the skill stays portable.
99
- - **Failure handling**: If `gh` auth expires or `oat project archive sync` fails, the cron run should log the failure and exit non-zero rather than producing an empty wrap-up. Prefer pinning the shell to `set -euo pipefail` or an equivalent in the wrapper.
99
+ - **Failure handling**: If `gh` auth expires or `oat repo archive sync` fails, the cron run should log the failure and exit non-zero rather than producing an empty wrap-up. Prefer pinning the shell to `set -euo pipefail` or an equivalent in the wrapper.
100
100
  - **Idempotence**: Running the same window twice produces the same report content (modulo `generated_at`). Overwriting is safe; appending duplicate commits is usually not — coalesce with a pre-check (`git diff --quiet` on the wrap-ups directory) if your cadence might cause no-op runs.
@@ -188,7 +188,7 @@ const CONFIG_CATALOG = [
188
188
  defaultValue: 'unset',
189
189
  mutability: 'read/write',
190
190
  owningCommand: 'oat config set archive.awsProfile <value>',
191
- description: 'AWS named profile forwarded as AWS_PROFILE to every `aws` invocation made by the archive S3 sync (completion + `oat project archive sync`). Precedence: per-invocation flag > existing shell env > this config value.',
191
+ description: 'AWS named profile forwarded as AWS_PROFILE to every `aws` invocation made by the archive S3 sync (completion + `oat repo archive sync`). Precedence: per-invocation flag > this config value > existing shell env.',
192
192
  },
193
193
  {
194
194
  key: 'archive.awsRegion',
@@ -199,7 +199,7 @@ const CONFIG_CATALOG = [
199
199
  defaultValue: 'unset',
200
200
  mutability: 'read/write',
201
201
  owningCommand: 'oat config set archive.awsRegion <value>',
202
- description: 'AWS region forwarded as AWS_REGION to every `aws` invocation made by the archive S3 sync (completion + `oat project archive sync`). Precedence: per-invocation flag > existing shell env > this config value.',
202
+ description: 'AWS region forwarded as AWS_REGION to every `aws` invocation made by the archive S3 sync (completion + `oat repo archive sync`). Precedence: per-invocation flag > this config value > existing shell env.',
203
203
  },
204
204
  {
205
205
  key: 'tools.brainstorm',
@@ -60,6 +60,23 @@ export interface ResolvePrimaryRepoRootDependencies {
60
60
  dirExists?: typeof dirExists;
61
61
  env?: NodeJS.ProcessEnv;
62
62
  }
63
+ export interface ResolveArchiveProjectTargetOptions {
64
+ repoRoot: string;
65
+ projectsRoot: string;
66
+ projectName: string;
67
+ }
68
+ export interface ResolveArchiveProjectTargetDependencies extends ResolvePrimaryRepoRootDependencies {
69
+ timestamp?: () => string;
70
+ }
71
+ export interface ArchiveProjectTarget {
72
+ archiveProjectPath: string;
73
+ archiveRepoRoot: string;
74
+ archivePath: string;
75
+ archivePathIsGitignored: boolean;
76
+ primaryRepoRoot: string | null;
77
+ primaryRepoRootAvailable: boolean;
78
+ localOnlyWarning: string | null;
79
+ }
63
80
  interface ArchiveProjectOnCompletionDependencies extends EnsureS3ArchiveAccessDependencies, ResolvePrimaryRepoRootDependencies {
64
81
  ensureS3ArchiveAccess?: typeof ensureS3ArchiveAccess;
65
82
  execFile?: ExecFileLike;
@@ -120,6 +137,8 @@ export declare function parseArchiveSnapshotName(snapshotName: string): {
120
137
  };
121
138
  export declare function resolveLocalArchiveProjectPath(projectsRoot: string, projectName: string): string;
122
139
  export declare function resolvePrimaryRepoRoot(repoRoot: string, dependencies?: ResolvePrimaryRepoRootDependencies): Promise<string>;
140
+ export declare function resolveArchiveProjectTarget(options: ResolveArchiveProjectTargetOptions, dependencies?: ResolveArchiveProjectTargetDependencies): Promise<ArchiveProjectTarget>;
141
+ export declare function assertDurableArchiveProjectTarget(target: ArchiveProjectTarget): void;
123
142
  export declare function archiveProjectOnCompletion(options: ArchiveProjectOnCompletionOptions, dependencies?: ArchiveProjectOnCompletionDependencies): Promise<ArchiveProjectOnCompletionResult>;
124
143
  export declare function ensureS3ArchiveAccess(options: EnsureS3ArchiveAccessOptions, dependencies?: EnsureS3ArchiveAccessDependencies): Promise<EnsureS3ArchiveAccessResult>;
125
144
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"archive-utils.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/archive-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"archive-utils.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/archive-utils.ts"],"names":[],"mappings":"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"}
@@ -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