@regressionproof/snapshotter 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.3.3](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.2...v0.3.3) (2026-01-13)
7
+
8
+ **Note:** Version bump only for package @regressionproof/snapshotter
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.3.2](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.1...v0.3.2) (2026-01-13)
15
+
16
+ **Note:** Version bump only for package @regressionproof/snapshotter
17
+
18
+
19
+
20
+
21
+
6
22
  ## [0.3.1](https://github.com/sprucelabsai-community/regressionproof/compare/v0.3.0...v0.3.1) (2026-01-13)
7
23
 
8
24
  **Note:** Version bump only for package @regressionproof/snapshotter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@regressionproof/snapshotter",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -49,6 +49,7 @@
49
49
  "@sprucelabs/esm-postbuild": "^9.0.13",
50
50
  "@sprucelabs/jest-json-reporter": "^10.0.19",
51
51
  "@sprucelabs/resolve-path-aliases": "^4.0.13",
52
+ "@sprucelabs/spruce-skill-utils": "^34.0.3",
52
53
  "@sprucelabs/test": "^11.1.0",
53
54
  "@sprucelabs/test-utils": "^7.2.3",
54
55
  "@types/node": "^25.0.6",
@@ -84,5 +85,11 @@
84
85
  "^#spruce/(.*)$": "<rootDir>/build/.spruce/$1"
85
86
  }
86
87
  },
87
- "gitHead": "192ac0ba411c3b6b41e3b24f58ec7748f5386d22"
88
+ "dependencies": {
89
+ "@sprucelabs/error": "^8.1.2",
90
+ "@sprucelabs/schema": "^33.1.3",
91
+ "@sprucelabs/spruce-core-schemas": "^42.1.3",
92
+ "@sprucelabs/spruce-skill-utils": "^34.0.3"
93
+ },
94
+ "gitHead": "bd2c5801a5d27ec36938d59dbfff50b32a390b87"
88
95
  }
@@ -0,0 +1,52 @@
1
+ import { default as SchemaEntity } from '@sprucelabs/schema'
2
+ import * as SpruceSchema from '@sprucelabs/schema'
3
+
4
+
5
+
6
+
7
+
8
+ export declare namespace SpruceErrors.RegressionproofSnapshotter {
9
+
10
+
11
+ export interface ExecCommandFailed {
12
+
13
+ /** . The command that was attempted */
14
+ 'command': string
15
+
16
+ 'stdout'?: string | undefined | null
17
+
18
+ 'stderr'?: string | undefined | null
19
+ }
20
+
21
+ export interface ExecCommandFailedSchema extends SpruceSchema.Schema {
22
+ id: 'execCommandFailed',
23
+ namespace: 'RegressionproofSnapshotter',
24
+ name: 'Exec command failed',
25
+ fields: {
26
+ /** . The command that was attempted */
27
+ 'command': {
28
+ type: 'text',
29
+ isRequired: true,
30
+ hint: 'The command that was attempted',
31
+ options: undefined
32
+ },
33
+ /** . */
34
+ 'stdout': {
35
+ type: 'text',
36
+ options: undefined
37
+ },
38
+ /** . */
39
+ 'stderr': {
40
+ type: 'text',
41
+ options: undefined
42
+ },
43
+ }
44
+ }
45
+
46
+ export type ExecCommandFailedEntity = SchemaEntity<SpruceErrors.RegressionproofSnapshotter.ExecCommandFailedSchema>
47
+
48
+ }
49
+
50
+
51
+
52
+
@@ -0,0 +1,10 @@
1
+ import { SpruceErrors } from "#spruce/errors/errors.types"
2
+ import { ErrorOptions as ISpruceErrorOptions} from "@sprucelabs/error"
3
+
4
+ export interface ExecCommandFailedErrorOptions extends SpruceErrors.RegressionproofSnapshotter.ExecCommandFailed, ISpruceErrorOptions {
5
+ code: 'EXEC_COMMAND_FAILED'
6
+ }
7
+
8
+ type ErrorOptions = | ExecCommandFailedErrorOptions
9
+
10
+ export default ErrorOptions
@@ -0,0 +1,33 @@
1
+ import { SchemaRegistry } from '@sprucelabs/schema'
2
+ import { SpruceErrors } from '../errors.types'
3
+
4
+
5
+
6
+ const execCommandFailedSchema: SpruceErrors.RegressionproofSnapshotter.ExecCommandFailedSchema = {
7
+ id: 'execCommandFailed',
8
+ namespace: 'RegressionproofSnapshotter',
9
+ name: 'Exec command failed',
10
+ fields: {
11
+ /** . The command that was attempted */
12
+ 'command': {
13
+ type: 'text',
14
+ isRequired: true,
15
+ hint: 'The command that was attempted',
16
+ options: undefined
17
+ },
18
+ /** . */
19
+ 'stdout': {
20
+ type: 'text',
21
+ options: undefined
22
+ },
23
+ /** . */
24
+ 'stderr': {
25
+ type: 'text',
26
+ options: undefined
27
+ },
28
+ }
29
+ }
30
+
31
+ SchemaRegistry.getInstance().trackSchema(execCommandFailedSchema)
32
+
33
+ export default execCommandFailedSchema
@@ -0,0 +1,6 @@
1
+ import { fieldClassMap } from '@sprucelabs/schema'
2
+
3
+
4
+
5
+
6
+ export default fieldClassMap
@@ -0,0 +1 @@
1
+ export { FieldDefinitions, FieldDefinitionMap, FieldValueTypeGeneratorMap, FieldMap } from '@sprucelabs/schema'
@@ -6,7 +6,9 @@
6
6
  "skill"
7
7
  ],
8
8
  "installed": [
9
- "test"
9
+ "test",
10
+ "schema",
11
+ "error"
10
12
  ],
11
13
  "writer": {
12
14
  "skipped": [
@@ -0,0 +1,29 @@
1
+ import BaseSpruceError from '@sprucelabs/error'
2
+ import ErrorOptions from '#spruce/errors/options.types'
3
+
4
+ export default class SpruceError extends BaseSpruceError<ErrorOptions> {
5
+ /** an easy to understand version of the errors */
6
+ public friendlyMessage(): string {
7
+ const { options } = this
8
+ let message
9
+ switch (options?.code) {
10
+ case 'EXEC_COMMAND_FAILED':
11
+ message = `The command "${options.command}" failed to execute.
12
+
13
+ Stdout:
14
+ ${options.stdout ?? '<no stdout>'}
15
+
16
+ Stderr:
17
+ ${options.stderr ?? '<no stderr>'}`
18
+ break
19
+ default:
20
+ message = super.friendlyMessage()
21
+ }
22
+
23
+ const fullMessage = options.friendlyMessage
24
+ ? options.friendlyMessage
25
+ : message
26
+
27
+ return fullMessage
28
+ }
29
+ }
@@ -0,0 +1,19 @@
1
+ import { buildErrorSchema } from '@sprucelabs/schema'
2
+
3
+ export default buildErrorSchema({
4
+ id: 'execCommandFailed',
5
+ name: 'Exec command failed',
6
+ fields: {
7
+ command: {
8
+ type: 'text',
9
+ isRequired: true,
10
+ hint: 'The command that was attempted',
11
+ },
12
+ stdout: {
13
+ type: 'text',
14
+ },
15
+ stderr: {
16
+ type: 'text',
17
+ },
18
+ },
19
+ })
package/src/git.ts CHANGED
@@ -2,47 +2,74 @@ import { exec } from 'child_process'
2
2
  import { existsSync } from 'fs'
3
3
  import path from 'path'
4
4
  import { promisify } from 'util'
5
+ import { Log } from '@sprucelabs/spruce-skill-utils'
6
+ import SpruceError from './errors/SpruceError.js'
5
7
  import { RemoteOptions } from './snapshotter.types.js'
6
8
 
7
9
  const execAsync = promisify(exec)
8
10
 
9
- export async function gitCommit(mirrorPath: string): Promise<boolean> {
11
+ export async function gitCommit(
12
+ mirrorPath: string,
13
+ log?: Log
14
+ ): Promise<boolean> {
10
15
  const gitDir = path.join(mirrorPath, '.git')
11
16
 
12
17
  if (!existsSync(gitDir)) {
13
- await execAsync(`git -C "${mirrorPath}" init`)
18
+ await execOrThrow(`git -C "${mirrorPath}" init`, log)
14
19
  }
15
20
 
16
- await execAsync(`git -C "${mirrorPath}" add -A`)
21
+ await execOrThrow(`git -C "${mirrorPath}" add -A`, log)
17
22
 
18
- const { stdout } = await execAsync(
23
+ const { stdout } = await execOrThrow(
19
24
  `git -C "${mirrorPath}" status --porcelain`
20
25
  )
21
26
  if (!stdout.trim()) {
27
+ log?.info('No changes detected for commit', mirrorPath)
22
28
  return false
23
29
  }
24
30
 
25
31
  const message = `Snapshot ${new Date().toISOString()}`
26
- await execAsync(`git -C "${mirrorPath}" commit -m "${message}"`)
32
+ await execOrThrow(`git -C "${mirrorPath}" commit -m "${message}"`, log)
27
33
  return true
28
34
  }
29
35
 
30
36
  export async function gitPush(
31
37
  mirrorPath: string,
32
- remote: RemoteOptions
38
+ remote: RemoteOptions,
39
+ log?: Log
33
40
  ): Promise<void> {
34
41
  const authedUrl = remote.url.replace('://', `://${remote.token}@`)
35
42
 
36
43
  try {
37
- await execAsync(`git -C "${mirrorPath}" remote get-url origin`)
38
- await execAsync(
44
+ await execOrThrow(`git -C "${mirrorPath}" remote get-url origin`, log)
45
+ await execOrThrow(
39
46
  `git -C "${mirrorPath}" remote set-url origin "${authedUrl}"`
40
47
  )
41
48
  } catch {
42
- await execAsync(
49
+ await execOrThrow(
43
50
  `git -C "${mirrorPath}" remote add origin "${authedUrl}"`
44
51
  )
45
52
  }
46
53
 
47
- await execAsync(`git -C "${mirrorPath}" push -u origin HEAD`)
54
+ await execOrThrow(`git -C "${mirrorPath}" push -u origin HEAD`, log)
55
+ }
56
+
57
+ async function execOrThrow(
58
+ command: string,
59
+ log?: Log
60
+ ): Promise<{ stdout: string; stderr: string }> {
61
+ try {
62
+ return await execAsync(command)
63
+ } catch (err) {
64
+ const error = err as Error & { stdout?: string; stderr?: string }
65
+ const stdout = error.stdout ?? ''
66
+ const stderr = error.stderr ?? ''
67
+ log?.error('Command failed', command, stderr)
68
+ throw new SpruceError({
69
+ code: 'EXEC_COMMAND_FAILED',
70
+ command,
71
+ stdout,
72
+ stderr,
73
+ })
74
+ }
48
75
  }
package/src/snapshot.ts CHANGED
@@ -1,27 +1,51 @@
1
1
  import { mkdirSync, writeFileSync } from 'fs'
2
2
  import path from 'path'
3
+ import { buildLog } from '@sprucelabs/spruce-skill-utils'
3
4
  import { gitCommit, gitPush } from './git.js'
4
5
  import { SnapshotOptions } from './snapshotter.types.js'
5
6
  import { syncFiles } from './sync.js'
6
7
 
7
- export async function snapshot(options: SnapshotOptions): Promise<boolean> {
8
- const sourcePath = options.sourcePath ?? process.cwd()
9
- const { mirrorPath, testResults, remote } = options
8
+ class Snapshotter {
9
+ private log = buildLog('Snapshotter')
10
+
11
+ public async snapshot(options: SnapshotOptions): Promise<boolean> {
12
+ const sourcePath = options.sourcePath ?? process.cwd()
13
+ const { mirrorPath, testResults, remote } = options
14
+
15
+ this.log.info('Starting snapshot', sourcePath, mirrorPath)
16
+
17
+ try {
18
+ await syncFiles(sourcePath, mirrorPath)
19
+ this.log.info('Files synced', mirrorPath)
10
20
 
11
- await syncFiles(sourcePath, mirrorPath)
21
+ const snapshotterDir = path.join(mirrorPath, '.snapshotter')
22
+ mkdirSync(snapshotterDir, { recursive: true })
23
+ writeFileSync(
24
+ path.join(snapshotterDir, 'testResults.json'),
25
+ JSON.stringify(testResults, null, 2)
26
+ )
27
+ this.log.info('Test results saved', snapshotterDir)
12
28
 
13
- const snapshotterDir = path.join(mirrorPath, '.snapshotter')
14
- mkdirSync(snapshotterDir, { recursive: true })
15
- writeFileSync(
16
- path.join(snapshotterDir, 'testResults.json'),
17
- JSON.stringify(testResults, null, 2)
18
- )
29
+ const committed = await gitCommit(mirrorPath, this.log)
19
30
 
20
- const committed = await gitCommit(mirrorPath)
31
+ if (!committed) {
32
+ this.log.info('No changes to commit', mirrorPath)
33
+ return false
34
+ }
21
35
 
22
- if (committed) {
23
- await gitPush(mirrorPath, remote)
36
+ this.log.info('Commit created, pushing', remote.url)
37
+ await gitPush(mirrorPath, remote, this.log)
38
+ this.log.info('Push completed', remote.url)
39
+
40
+ return true
41
+ } catch (err) {
42
+ const message = err instanceof Error ? err.message : String(err)
43
+ this.log.error('Snapshot failed', message)
44
+ throw err
45
+ }
24
46
  }
47
+ }
25
48
 
26
- return committed
49
+ export async function snapshot(options: SnapshotOptions): Promise<boolean> {
50
+ return new Snapshotter().snapshot(options)
27
51
  }