create-appraisejs 0.2.0-alpha.4 → 0.2.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -52
- package/package.json +1 -1
- package/templates/default/.appraise-template-meta.json +2 -2
- package/templates/default/package-lock.json +2 -2
- package/templates/default/prisma/dev.db +0 -0
- package/templates/default/public/favicon.svg +11 -0
- package/templates/default/public/logo.svg +18 -0
- package/templates/default/src/app/favicon.ico +0 -0
- package/templates/default/src/app/layout.tsx +60 -20
- package/templates/default/src/components/logo.tsx +15 -15
- package/templates/default/src/config/db-config.ts +4 -4
- package/templates/default/src/lib/automation/projection-service.ts +0 -5
- package/templates/default/src/lib/environment-file-utils.ts +143 -143
- package/templates/default/src/lib/gherkin-parser.test.ts +44 -44
- package/templates/default/src/lib/utils/template-step-file-generator.ts +128 -104
package/README.md
CHANGED
|
@@ -1,52 +1,102 @@
|
|
|
1
|
-
# create-appraisejs
|
|
2
|
-
|
|
3
|
-
Scaffold a new [
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx create-appraisejs@latest
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
The CLI will
|
|
12
|
-
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
# create-appraisejs
|
|
2
|
+
|
|
3
|
+
Scaffold a new [AppraiseJS](https://github.com/jamil2018/appraisejs-core) project.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-appraisejs@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI will ask for:
|
|
12
|
+
|
|
13
|
+
1. The target directory. It must not exist yet, or it must be empty.
|
|
14
|
+
2. The package manager: `npm`, `pnpm`, `yarn`, or `bun`.
|
|
15
|
+
3. Whether to run the production setup immediately.
|
|
16
|
+
4. Which Playwright browsers you want available: `chromium`, `firefox`, and/or `webkit`.
|
|
17
|
+
|
|
18
|
+
## What The Scaffolder Does
|
|
19
|
+
|
|
20
|
+
By default, `create-appraisejs` uses the bundled template shipped inside the package.
|
|
21
|
+
|
|
22
|
+
During scaffolding it:
|
|
23
|
+
|
|
24
|
+
1. Copies the packaged AppraiseJS template into your target directory.
|
|
25
|
+
2. Renames the packaged `gitignore` file back to `.gitignore`.
|
|
26
|
+
3. Rewrites `package.json` scripts so they use your chosen package manager.
|
|
27
|
+
4. Preserves the seeded local SQLite database at `prisma/dev.db`.
|
|
28
|
+
5. Starts you with a clean automation workspace: `automation/config/environments/environments.json` is reset to `{}`, `automation/mapping/locator-map.json` is reset to `[]`, reusable step definitions are included, and starter features, locators, and reports are not bundled into the generated app.
|
|
29
|
+
6. Optionally runs the project's `setup` script and then installs any Playwright browsers you selected.
|
|
30
|
+
|
|
31
|
+
If you skip setup, the CLI still prints the exact next commands to run.
|
|
32
|
+
|
|
33
|
+
## Default Local Workflow
|
|
34
|
+
|
|
35
|
+
From the generated project directory:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Install dependencies, create .env, prepare the database, and build the app
|
|
39
|
+
npm run setup
|
|
40
|
+
|
|
41
|
+
# Optional: install only the browsers you need
|
|
42
|
+
npm run install-playwright -- chromium
|
|
43
|
+
|
|
44
|
+
# Start the local production server
|
|
45
|
+
npm run start
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`npm run dev` is still available, but the scaffold is intentionally production-first.
|
|
49
|
+
|
|
50
|
+
## Generated Project Highlights
|
|
51
|
+
|
|
52
|
+
The generated project includes:
|
|
53
|
+
|
|
54
|
+
- a seeded SQLite database at `prisma/dev.db`
|
|
55
|
+
- the AppraiseJS dashboard and application code
|
|
56
|
+
- automation sync scripts and reusable step definitions
|
|
57
|
+
- package-manager-aware scripts such as `setup`, `setup:db`, `setup:full`, and `appraisejs:sync`
|
|
58
|
+
|
|
59
|
+
The generated project does not include:
|
|
60
|
+
|
|
61
|
+
- a ready-made `.env` file
|
|
62
|
+
- starter feature files under `automation/features`
|
|
63
|
+
- starter locator files under `automation/locators`
|
|
64
|
+
- automation reports
|
|
65
|
+
|
|
66
|
+
## Template Source Overrides
|
|
67
|
+
|
|
68
|
+
The package defaults to the bundled template. Remote fetching is only used when you provide one of the override environment variables below.
|
|
69
|
+
|
|
70
|
+
| Variable | Description | Default |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `CREATE_APPRAISE_REPO_URL` | Repository URL used for remote template fetching. | `https://github.com/jamil2018/appraisejs-core.git` |
|
|
73
|
+
| `CREATE_APPRAISE_BRANCH` | Branch or ref to fetch from the remote repository. | `main` |
|
|
74
|
+
| `CREATE_APPRAISE_TEMPLATE_SUBPATH` | Path to the template directory inside that repository. | `templates/default` |
|
|
75
|
+
| `CREATE_APPRAISE_USE_BUNDLED` | Set to `1`, `true`, or `yes` to force the bundled template even when remote overrides are present. | bundled template |
|
|
76
|
+
|
|
77
|
+
When remote mode is active, the CLI tries the repository tarball first and falls back to `git clone` if needed.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
CREATE_APPRAISE_BRANCH=main CREATE_APPRAISE_TEMPLATE_SUBPATH=templates/default npx create-appraisejs@latest
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Common Scripts In The Generated App
|
|
86
|
+
|
|
87
|
+
| Script | What it does |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `npm run setup` | Install dependencies, create `.env`, rebuild the local DB, build the app, and protect seeded files |
|
|
90
|
+
| `npm run setup:db` | Recreate the local SQLite database from migrations and rerun the sync pipeline |
|
|
91
|
+
| `npm run setup:full` | Reinstall dependencies, rebuild the DB, rebuild the app, and protect seeded files |
|
|
92
|
+
| `npm run install-playwright -- <browser...>` | Install selected Playwright browsers |
|
|
93
|
+
| `npm run sync-all` | Run the full sync pipeline |
|
|
94
|
+
| `npm run appraisejs:sync` | Alias for `sync-all` |
|
|
95
|
+
| `npm run start` | Start the local production server |
|
|
96
|
+
| `npm run dev` | Start the Next.js development server |
|
|
97
|
+
|
|
98
|
+
## Notes
|
|
99
|
+
|
|
100
|
+
- Node.js `18+` is required.
|
|
101
|
+
- The CLI rewrites hardcoded `npm` and `npx` usage inside the generated scripts so `pnpm`, `yarn`, and `bun` work correctly after scaffolding.
|
|
102
|
+
- Selecting Playwright browsers in the prompt does not force installation unless you also choose to run setup immediately. If you skip setup, the CLI shows the browser install command in the next steps.
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"preparedAt": "2026-03-
|
|
3
|
-
"inputHash": "
|
|
2
|
+
"preparedAt": "2026-03-25T13:42:37.013Z",
|
|
3
|
+
"inputHash": "3d8e428a4dcbb25e993b9373c10b2a974387982a734dcfc9281c24bb38f6f36d",
|
|
4
4
|
"databasePath": "prisma/dev.db"
|
|
5
5
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appraise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appraise",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.2.0-alpha",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@babel/parser": "^7.27.0",
|
|
12
12
|
"@babel/types": "^7.27.0",
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
|
2
|
+
<circle class="ring" cx="64" cy="64" r="50" fill="none" stroke-width="5.5"/>
|
|
3
|
+
<polyline class="ck" points="38,66 55,83 92,41" fill="none" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<style>
|
|
5
|
+
.ring{stroke:#d4d4d4}
|
|
6
|
+
.ck{stroke:#5cb85c}
|
|
7
|
+
@media(prefers-color-scheme:light){
|
|
8
|
+
.ring{stroke:#bbb}
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
|
2
|
+
<line class="ln" x1="14" y1="64" x2="62" y2="64" stroke-width="4.5" stroke-linecap="round"/>
|
|
3
|
+
<circle class="dot" cx="16" cy="64" r="6.5" stroke-width="3.5"/>
|
|
4
|
+
<circle class="dot" cx="42" cy="64" r="8.5" stroke-width="3.5"/>
|
|
5
|
+
<circle class="ring" cx="88" cy="64" r="26" fill="none" stroke-width="7"/>
|
|
6
|
+
<polyline class="ck" points="76,66 85,76 102,54" fill="none" stroke-width="6.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
<style>
|
|
8
|
+
.ln{stroke:#d4d4d4}
|
|
9
|
+
.dot{fill:#999;stroke:#d4d4d4}
|
|
10
|
+
.ring{stroke:#eee}
|
|
11
|
+
.ck{stroke:#5cb85c}
|
|
12
|
+
@media(prefers-color-scheme:light){
|
|
13
|
+
.ln{stroke:#555}
|
|
14
|
+
.dot{fill:#aaa;stroke:#555}
|
|
15
|
+
.ring{stroke:#333}
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
</svg>
|
|
Binary file
|
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
import type { Metadata } from 'next'
|
|
1
|
+
import type { Metadata, Viewport } from 'next'
|
|
2
2
|
import { Inter, Inter_Tight } from 'next/font/google'
|
|
3
3
|
import './globals.css'
|
|
4
4
|
import { ThemeProvider } from '@/components/theme/theme-provider'
|
|
5
5
|
import { ModeToggle } from '@/components/theme/mode-toggle'
|
|
6
6
|
import { Toaster } from '@/components/ui/toaster'
|
|
7
|
-
|
|
8
|
-
const inter = Inter({
|
|
9
|
-
variable: '--font-inter',
|
|
10
|
-
subsets: ['latin'],
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
const interTight = Inter_Tight({
|
|
14
|
-
variable: '--font-inter-tight',
|
|
15
|
-
subsets: ['latin'],
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
export const metadata: Metadata = {
|
|
19
|
-
title: 'Appraise | Dashboard',
|
|
20
|
-
description: 'Welcome to the dashboard. Here you can see your test suites and run them.',
|
|
21
|
-
}
|
|
22
|
-
|
|
23
7
|
import Logo from '@/components/logo'
|
|
8
|
+
import Link from 'next/link'
|
|
9
|
+
import NavMenuCardDeck from '@/components/navigation/nav-menu-card-deck'
|
|
10
|
+
import NavCommand from '@/components/navigation/nav-command'
|
|
24
11
|
import NavLink from '@/components/navigation/nav-link'
|
|
25
12
|
import {
|
|
26
13
|
Blocks,
|
|
@@ -41,9 +28,62 @@ import {
|
|
|
41
28
|
TestTubeDiagonal,
|
|
42
29
|
TestTubes,
|
|
43
30
|
} from 'lucide-react'
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
|
|
32
|
+
const inter = Inter({
|
|
33
|
+
variable: '--font-inter',
|
|
34
|
+
subsets: ['latin'],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const interTight = Inter_Tight({
|
|
38
|
+
variable: '--font-inter-tight',
|
|
39
|
+
subsets: ['latin'],
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const appTitle = 'AppraiseJS'
|
|
43
|
+
const appDescription =
|
|
44
|
+
'AppraiseJS helps teams organize automated tests, execute suites, and review results from one dashboard.'
|
|
45
|
+
|
|
46
|
+
export const viewport: Viewport = {
|
|
47
|
+
colorScheme: 'light dark',
|
|
48
|
+
themeColor: [
|
|
49
|
+
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
|
|
50
|
+
{ media: '(prefers-color-scheme: dark)', color: '#09090b' },
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const metadata: Metadata = {
|
|
55
|
+
title: {
|
|
56
|
+
default: appTitle,
|
|
57
|
+
template: `%s | ${appTitle}`,
|
|
58
|
+
},
|
|
59
|
+
description: appDescription,
|
|
60
|
+
applicationName: appTitle,
|
|
61
|
+
keywords: ['AppraiseJS', 'test automation', 'QA', 'dashboard', 'test execution'],
|
|
62
|
+
openGraph: {
|
|
63
|
+
title: appTitle,
|
|
64
|
+
description: appDescription,
|
|
65
|
+
siteName: appTitle,
|
|
66
|
+
type: 'website',
|
|
67
|
+
},
|
|
68
|
+
twitter: {
|
|
69
|
+
card: 'summary',
|
|
70
|
+
title: appTitle,
|
|
71
|
+
description: appDescription,
|
|
72
|
+
},
|
|
73
|
+
appleWebApp: {
|
|
74
|
+
capable: true,
|
|
75
|
+
title: appTitle,
|
|
76
|
+
statusBarStyle: 'default',
|
|
77
|
+
},
|
|
78
|
+
icons: {
|
|
79
|
+
icon: [
|
|
80
|
+
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
|
81
|
+
{ url: '/favicon.ico', sizes: 'any' },
|
|
82
|
+
],
|
|
83
|
+
shortcut: ['/favicon.svg'],
|
|
84
|
+
other: [{ rel: 'mask-icon', url: '/favicon.svg', color: '#5cb85c' }],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
47
87
|
|
|
48
88
|
export default function RootLayout({
|
|
49
89
|
children,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const Logo = () => {
|
|
4
|
-
return (
|
|
5
|
-
<div className="m-2 flex items-center">
|
|
6
|
-
<
|
|
7
|
-
<span className="
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</div>
|
|
12
|
-
)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default Logo
|
|
1
|
+
import Image from 'next/image'
|
|
2
|
+
|
|
3
|
+
const Logo = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="m-2 flex items-center gap-1">
|
|
6
|
+
<Image src="/logo.svg" alt="AppraiseJS" width={32} height={32} priority className="h-8 w-8" />
|
|
7
|
+
<span className="flex items-baseline gap-1">
|
|
8
|
+
<span className="text-foreground/90 text-[0.95rem] font-medium uppercase tracking-[0.12em]">Appraise</span>
|
|
9
|
+
<span className="text-[0.95rem] font-normal uppercase tracking-[0.12em] text-primary">JS</span>
|
|
10
|
+
</span>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default Logo
|
|
@@ -65,7 +65,7 @@ const globalForPrisma = global as unknown as {
|
|
|
65
65
|
prisma: PrismaClientInstance | undefined
|
|
66
66
|
}
|
|
67
67
|
const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
68
|
-
|
|
69
|
-
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
70
|
-
|
|
71
|
-
export default prisma
|
|
68
|
+
|
|
69
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
|
70
|
+
|
|
71
|
+
export default prisma
|
|
@@ -214,14 +214,9 @@ class AutomationProjectionService {
|
|
|
214
214
|
select: { id: true },
|
|
215
215
|
})
|
|
216
216
|
|
|
217
|
-
const templateStepGroups = await prisma.templateStepGroup.findMany({
|
|
218
|
-
select: { id: true },
|
|
219
|
-
})
|
|
220
|
-
|
|
221
217
|
await Promise.all([
|
|
222
218
|
this.syncEnvironments(),
|
|
223
219
|
...locatorGroups.map(locatorGroup => this.syncLocatorGroup(locatorGroup.id)),
|
|
224
|
-
...templateStepGroups.map(group => this.syncTemplateStepGroup(group.id)),
|
|
225
220
|
])
|
|
226
221
|
|
|
227
222
|
await this.regenerateAllFeatures()
|
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs'
|
|
|
2
2
|
import * as path from 'path'
|
|
3
3
|
import prisma from '@/config/db-config'
|
|
4
4
|
import { ensureAutomationWorkspaceReady, getAutomationEnvironmentsDir } from '@/lib/automation/paths'
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
interface EnvironmentConfig {
|
|
7
7
|
baseUrl: string
|
|
8
8
|
apiBaseUrl: string
|
|
@@ -11,47 +11,47 @@ interface EnvironmentConfig {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const EMPTY_ENVIRONMENTS_FILE_CONTENT = '{}\n'
|
|
14
|
-
|
|
15
|
-
export function getEnvironmentsFilePath(): string {
|
|
16
|
-
return path.join(getAutomationEnvironmentsDir(), 'environments.json')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function ensureConfigDirectoryExists(): Promise<void> {
|
|
20
|
-
await ensureAutomationWorkspaceReady()
|
|
21
|
-
await fs.mkdir(path.dirname(getEnvironmentsFilePath()), { recursive: true })
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function generateEnvironmentsContent(): Promise<Record<string, EnvironmentConfig>> {
|
|
25
|
-
try {
|
|
26
|
-
const environments = await prisma.environment.findMany({
|
|
27
|
-
orderBy: { createdAt: 'asc' },
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const environmentsConfig: Record<string, EnvironmentConfig> = {}
|
|
31
|
-
|
|
32
|
-
environments.forEach(env => {
|
|
33
|
-
const envKey = env.name.toLowerCase().replace(/\s+/g, '_')
|
|
34
|
-
environmentsConfig[envKey] = {
|
|
35
|
-
baseUrl: env.baseUrl,
|
|
36
|
-
apiBaseUrl: env.apiBaseUrl || '',
|
|
37
|
-
email: env.username || '',
|
|
38
|
-
password: env.password || '',
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return environmentsConfig
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error('Error generating environments content:', error)
|
|
45
|
-
return {}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
14
|
+
|
|
15
|
+
export function getEnvironmentsFilePath(): string {
|
|
16
|
+
return path.join(getAutomationEnvironmentsDir(), 'environments.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureConfigDirectoryExists(): Promise<void> {
|
|
20
|
+
await ensureAutomationWorkspaceReady()
|
|
21
|
+
await fs.mkdir(path.dirname(getEnvironmentsFilePath()), { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateEnvironmentsContent(): Promise<Record<string, EnvironmentConfig>> {
|
|
25
|
+
try {
|
|
26
|
+
const environments = await prisma.environment.findMany({
|
|
27
|
+
orderBy: { createdAt: 'asc' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const environmentsConfig: Record<string, EnvironmentConfig> = {}
|
|
31
|
+
|
|
32
|
+
environments.forEach(env => {
|
|
33
|
+
const envKey = env.name.toLowerCase().replace(/\s+/g, '_')
|
|
34
|
+
environmentsConfig[envKey] = {
|
|
35
|
+
baseUrl: env.baseUrl,
|
|
36
|
+
apiBaseUrl: env.apiBaseUrl || '',
|
|
37
|
+
email: env.username || '',
|
|
38
|
+
password: env.password || '',
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return environmentsConfig
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error generating environments content:', error)
|
|
45
|
+
return {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
49
|
export async function createOrUpdateEnvironmentsFile(): Promise<boolean> {
|
|
50
50
|
try {
|
|
51
51
|
await ensureAutomationWorkspaceReady()
|
|
52
52
|
const filePath = getEnvironmentsFilePath()
|
|
53
53
|
await ensureConfigDirectoryExists()
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
const content = await generateEnvironmentsContent()
|
|
56
56
|
|
|
57
57
|
if (Object.keys(content).length === 0) {
|
|
@@ -61,12 +61,12 @@ export async function createOrUpdateEnvironmentsFile(): Promise<boolean> {
|
|
|
61
61
|
|
|
62
62
|
await fs.writeFile(filePath, JSON.stringify(content, null, 2))
|
|
63
63
|
return true
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error('Error creating/updating environments file:', error)
|
|
66
|
-
return false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error creating/updating environments file:', error)
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
70
|
export async function deleteEnvironmentsFile(): Promise<boolean> {
|
|
71
71
|
try {
|
|
72
72
|
await ensureAutomationWorkspaceReady()
|
|
@@ -78,103 +78,103 @@ export async function deleteEnvironmentsFile(): Promise<boolean> {
|
|
|
78
78
|
console.error('Error deleting environments file:', error)
|
|
79
79
|
return false
|
|
80
80
|
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function readEnvironmentsFile(): Promise<{
|
|
84
|
-
filePath: string
|
|
85
|
-
content: Record<string, EnvironmentConfig>
|
|
86
|
-
} | null> {
|
|
87
|
-
try {
|
|
88
|
-
await ensureAutomationWorkspaceReady()
|
|
89
|
-
const filePath = getEnvironmentsFilePath()
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await fs.access(filePath)
|
|
93
|
-
} catch {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
98
|
-
const jsonContent = JSON.parse(fileContent)
|
|
99
|
-
|
|
100
|
-
return { filePath, content: jsonContent }
|
|
101
|
-
} catch (error) {
|
|
102
|
-
console.error('Error reading environments file:', error)
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function updateEnvironmentEntry(environmentId: string, oldName?: string): Promise<boolean> {
|
|
108
|
-
try {
|
|
109
|
-
await ensureAutomationWorkspaceReady()
|
|
110
|
-
const environment = await prisma.environment.findUnique({
|
|
111
|
-
where: { id: environmentId },
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
if (!environment) {
|
|
115
|
-
console.error(`Environment with ID ${environmentId} not found`)
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const filePath = getEnvironmentsFilePath()
|
|
120
|
-
let environmentsConfig: Record<string, EnvironmentConfig> = {}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
await fs.access(filePath)
|
|
124
|
-
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
125
|
-
environmentsConfig = JSON.parse(fileContent)
|
|
126
|
-
} catch {
|
|
127
|
-
environmentsConfig = {}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (oldName) {
|
|
131
|
-
const oldKey = oldName.toLowerCase().replace(/\s+/g, '_')
|
|
132
|
-
delete environmentsConfig[oldKey]
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const envKey = environment.name.toLowerCase().replace(/\s+/g, '_')
|
|
136
|
-
environmentsConfig[envKey] = {
|
|
137
|
-
baseUrl: environment.baseUrl,
|
|
138
|
-
apiBaseUrl: environment.apiBaseUrl || '',
|
|
139
|
-
email: environment.username || '',
|
|
140
|
-
password: environment.password || '',
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
await ensureConfigDirectoryExists()
|
|
144
|
-
await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
|
|
145
|
-
return true
|
|
146
|
-
} catch (error) {
|
|
147
|
-
console.error('Error updating environment entry:', error)
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function removeEnvironmentEntry(environmentName: string): Promise<boolean> {
|
|
153
|
-
try {
|
|
154
|
-
await ensureAutomationWorkspaceReady()
|
|
155
|
-
const filePath = getEnvironmentsFilePath()
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
await fs.access(filePath)
|
|
159
|
-
} catch {
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
164
|
-
const environmentsConfig: Record<string, EnvironmentConfig> = JSON.parse(fileContent)
|
|
165
|
-
|
|
166
|
-
const envKey = environmentName.toLowerCase().replace(/\s+/g, '_')
|
|
167
|
-
delete environmentsConfig[envKey]
|
|
168
|
-
|
|
169
|
-
if (Object.keys(environmentsConfig).length === 0) {
|
|
170
|
-
await deleteEnvironmentsFile()
|
|
171
|
-
return true
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
|
|
175
|
-
return true
|
|
176
|
-
} catch (error) {
|
|
177
|
-
console.error('Error removing environment entry:', error)
|
|
178
|
-
return false
|
|
179
|
-
}
|
|
180
|
-
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function readEnvironmentsFile(): Promise<{
|
|
84
|
+
filePath: string
|
|
85
|
+
content: Record<string, EnvironmentConfig>
|
|
86
|
+
} | null> {
|
|
87
|
+
try {
|
|
88
|
+
await ensureAutomationWorkspaceReady()
|
|
89
|
+
const filePath = getEnvironmentsFilePath()
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await fs.access(filePath)
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
98
|
+
const jsonContent = JSON.parse(fileContent)
|
|
99
|
+
|
|
100
|
+
return { filePath, content: jsonContent }
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Error reading environments file:', error)
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function updateEnvironmentEntry(environmentId: string, oldName?: string): Promise<boolean> {
|
|
108
|
+
try {
|
|
109
|
+
await ensureAutomationWorkspaceReady()
|
|
110
|
+
const environment = await prisma.environment.findUnique({
|
|
111
|
+
where: { id: environmentId },
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!environment) {
|
|
115
|
+
console.error(`Environment with ID ${environmentId} not found`)
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const filePath = getEnvironmentsFilePath()
|
|
120
|
+
let environmentsConfig: Record<string, EnvironmentConfig> = {}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await fs.access(filePath)
|
|
124
|
+
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
125
|
+
environmentsConfig = JSON.parse(fileContent)
|
|
126
|
+
} catch {
|
|
127
|
+
environmentsConfig = {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (oldName) {
|
|
131
|
+
const oldKey = oldName.toLowerCase().replace(/\s+/g, '_')
|
|
132
|
+
delete environmentsConfig[oldKey]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const envKey = environment.name.toLowerCase().replace(/\s+/g, '_')
|
|
136
|
+
environmentsConfig[envKey] = {
|
|
137
|
+
baseUrl: environment.baseUrl,
|
|
138
|
+
apiBaseUrl: environment.apiBaseUrl || '',
|
|
139
|
+
email: environment.username || '',
|
|
140
|
+
password: environment.password || '',
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await ensureConfigDirectoryExists()
|
|
144
|
+
await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
|
|
145
|
+
return true
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Error updating environment entry:', error)
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function removeEnvironmentEntry(environmentName: string): Promise<boolean> {
|
|
153
|
+
try {
|
|
154
|
+
await ensureAutomationWorkspaceReady()
|
|
155
|
+
const filePath = getEnvironmentsFilePath()
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(filePath)
|
|
159
|
+
} catch {
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
164
|
+
const environmentsConfig: Record<string, EnvironmentConfig> = JSON.parse(fileContent)
|
|
165
|
+
|
|
166
|
+
const envKey = environmentName.toLowerCase().replace(/\s+/g, '_')
|
|
167
|
+
delete environmentsConfig[envKey]
|
|
168
|
+
|
|
169
|
+
if (Object.keys(environmentsConfig).length === 0) {
|
|
170
|
+
await deleteEnvironmentsFile()
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
|
|
175
|
+
return true
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('Error removing environment entry:', error)
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { promises as fs } from 'fs'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { tmpdir } from 'os'
|
|
6
|
-
import { parseFeatureFile } from '@/lib/gherkin-parser'
|
|
7
|
-
|
|
8
|
-
async function withTempFeatureFile(content: string): Promise<string> {
|
|
9
|
-
const dir = await fs.mkdtemp(join(tmpdir(), 'gherkin-parser-'))
|
|
10
|
-
const filePath = join(dir, 'sample.feature')
|
|
11
|
-
await fs.writeFile(filePath, content, 'utf8')
|
|
12
|
-
return filePath
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
test('uses Feature line text as feature description', async () => {
|
|
16
|
-
const filePath = await withTempFeatureFile(`
|
|
17
|
-
@smoke
|
|
18
|
-
Feature: Login workflow
|
|
19
|
-
|
|
20
|
-
Scenario: logs in
|
|
21
|
-
Given user opens app
|
|
22
|
-
`)
|
|
23
|
-
|
|
24
|
-
const parsed = await parseFeatureFile(filePath)
|
|
25
|
-
|
|
26
|
-
assert.ok(parsed)
|
|
27
|
-
assert.equal(parsed?.featureName, 'Login workflow')
|
|
28
|
-
assert.equal(parsed?.featureDescription, 'Login workflow')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('keeps Feature line as description even when free text follows', async () => {
|
|
32
|
-
const filePath = await withTempFeatureFile(`
|
|
33
|
-
Feature: Checkout flow
|
|
34
|
-
Legacy block text that should not override the description
|
|
35
|
-
|
|
36
|
-
Scenario: buys item
|
|
37
|
-
Given user adds item to cart
|
|
38
|
-
`)
|
|
39
|
-
|
|
40
|
-
const parsed = await parseFeatureFile(filePath)
|
|
41
|
-
|
|
42
|
-
assert.ok(parsed)
|
|
43
|
-
assert.equal(parsed?.featureDescription, 'Checkout flow')
|
|
44
|
-
})
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { promises as fs } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { tmpdir } from 'os'
|
|
6
|
+
import { parseFeatureFile } from '@/lib/gherkin-parser'
|
|
7
|
+
|
|
8
|
+
async function withTempFeatureFile(content: string): Promise<string> {
|
|
9
|
+
const dir = await fs.mkdtemp(join(tmpdir(), 'gherkin-parser-'))
|
|
10
|
+
const filePath = join(dir, 'sample.feature')
|
|
11
|
+
await fs.writeFile(filePath, content, 'utf8')
|
|
12
|
+
return filePath
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('uses Feature line text as feature description', async () => {
|
|
16
|
+
const filePath = await withTempFeatureFile(`
|
|
17
|
+
@smoke
|
|
18
|
+
Feature: Login workflow
|
|
19
|
+
|
|
20
|
+
Scenario: logs in
|
|
21
|
+
Given user opens app
|
|
22
|
+
`)
|
|
23
|
+
|
|
24
|
+
const parsed = await parseFeatureFile(filePath)
|
|
25
|
+
|
|
26
|
+
assert.ok(parsed)
|
|
27
|
+
assert.equal(parsed?.featureName, 'Login workflow')
|
|
28
|
+
assert.equal(parsed?.featureDescription, 'Login workflow')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('keeps Feature line as description even when free text follows', async () => {
|
|
32
|
+
const filePath = await withTempFeatureFile(`
|
|
33
|
+
Feature: Checkout flow
|
|
34
|
+
Legacy block text that should not override the description
|
|
35
|
+
|
|
36
|
+
Scenario: buys item
|
|
37
|
+
Given user adds item to cart
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
const parsed = await parseFeatureFile(filePath)
|
|
41
|
+
|
|
42
|
+
assert.ok(parsed)
|
|
43
|
+
assert.equal(parsed?.featureDescription, 'Checkout flow')
|
|
44
|
+
})
|
|
@@ -1,104 +1,128 @@
|
|
|
1
|
-
import { promises as fs } from 'fs'
|
|
2
|
-
import { join } from 'path'
|
|
3
|
-
import prettier from 'prettier'
|
|
4
|
-
import { TemplateStep, TemplateStepGroupType } from '@prisma/client'
|
|
5
|
-
import {
|
|
6
|
-
ensureAutomationWorkspaceReady,
|
|
7
|
-
getAutomationActionStepsDir,
|
|
8
|
-
getAutomationStepsDir,
|
|
9
|
-
getAutomationValidationStepsDir,
|
|
10
|
-
} from '@/lib/automation/paths'
|
|
11
|
-
|
|
12
|
-
const RUNTIME_IMPORT = '../../../packages/cucumber-runtime/src/index.js'
|
|
13
|
-
const REQUIRED_RUNTIME_IMPORT =
|
|
14
|
-
`import { When, Then, CustomWorld, expect, SelectorName, resolveLocator, getEnvironment, generateRandomData, RandomDataType } from '${RUNTIME_IMPORT}';\n\n`
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
1
|
+
import { promises as fs } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import prettier from 'prettier'
|
|
4
|
+
import { TemplateStep, TemplateStepGroupType } from '@prisma/client'
|
|
5
|
+
import {
|
|
6
|
+
ensureAutomationWorkspaceReady,
|
|
7
|
+
getAutomationActionStepsDir,
|
|
8
|
+
getAutomationStepsDir,
|
|
9
|
+
getAutomationValidationStepsDir,
|
|
10
|
+
} from '@/lib/automation/paths'
|
|
11
|
+
|
|
12
|
+
const RUNTIME_IMPORT = '../../../packages/cucumber-runtime/src/index.js'
|
|
13
|
+
const REQUIRED_RUNTIME_IMPORT =
|
|
14
|
+
`import { When, Then, CustomWorld, expect, SelectorName, resolveLocator, getEnvironment, generateRandomData, RandomDataType } from '${RUNTIME_IMPORT}';\n\n`
|
|
15
|
+
|
|
16
|
+
function generateStepJSDoc(templateStep: Pick<TemplateStep, 'name' | 'description' | 'icon'>): string {
|
|
17
|
+
const lines = ['/**']
|
|
18
|
+
lines.push(` * @name ${templateStep.name}`)
|
|
19
|
+
if (templateStep.description) {
|
|
20
|
+
lines.push(` * @description ${templateStep.description}`)
|
|
21
|
+
}
|
|
22
|
+
lines.push(` * @icon ${templateStep.icon}`)
|
|
23
|
+
lines.push(' */')
|
|
24
|
+
return lines.join('\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripLeadingJSDoc(functionDefinition: string): string {
|
|
28
|
+
return functionDefinition.replace(/^\s*\/\*\*[\s\S]*?\*\/\s*/u, '').trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateStepDefinition(templateStep: TemplateStep): string | null {
|
|
32
|
+
const functionDefinition = templateStep.functionDefinition?.trim()
|
|
33
|
+
if (!functionDefinition) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${generateStepJSDoc(templateStep)}\n${stripLeadingJSDoc(functionDefinition)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sanitizeFileName(groupName: string): string {
|
|
41
|
+
return groupName
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/\s+/g, '_')
|
|
45
|
+
.replace(/[^a-z0-9_]/g, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateFileContent(templateSteps: TemplateStep[]): string {
|
|
49
|
+
if (!templateSteps || templateSteps.length === 0) {
|
|
50
|
+
return REQUIRED_RUNTIME_IMPORT + '// This file is generated automatically. Add template steps to this group to generate content.'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const functionDefinitions = templateSteps
|
|
54
|
+
.map(generateStepDefinition)
|
|
55
|
+
.filter((definition): definition is string => Boolean(definition))
|
|
56
|
+
.join('\n\n')
|
|
57
|
+
|
|
58
|
+
return REQUIRED_RUNTIME_IMPORT + functionDefinitions
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function formatFileContent(content: string): Promise<string> {
|
|
62
|
+
try {
|
|
63
|
+
return await prettier.format(content, {
|
|
64
|
+
parser: 'typescript',
|
|
65
|
+
semi: true,
|
|
66
|
+
singleQuote: true,
|
|
67
|
+
trailingComma: 'es5',
|
|
68
|
+
printWidth: 80,
|
|
69
|
+
tabWidth: 2,
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Prettier formatting failed:', error)
|
|
73
|
+
return content
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getSubdirectoryName(type: TemplateStepGroupType | string): string {
|
|
78
|
+
const typeStr = String(type)
|
|
79
|
+
return typeStr === 'ACTION' ? 'actions' : 'validations'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function ensureStepsDirectory(): Promise<string> {
|
|
83
|
+
await ensureAutomationWorkspaceReady()
|
|
84
|
+
const stepsDir = getAutomationStepsDir()
|
|
85
|
+
await fs.mkdir(stepsDir, { recursive: true })
|
|
86
|
+
await fs.mkdir(getAutomationActionStepsDir(), { recursive: true })
|
|
87
|
+
await fs.mkdir(getAutomationValidationStepsDir(), { recursive: true })
|
|
88
|
+
return stepsDir
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getFilePath(groupName: string, type: TemplateStepGroupType | string): string {
|
|
92
|
+
const sanitizedName = sanitizeFileName(groupName)
|
|
93
|
+
const subdirectory = getSubdirectoryName(type)
|
|
94
|
+
return join(process.cwd(), 'automation', 'steps', subdirectory, `${sanitizedName}.step.ts`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function writeTemplateStepFile(
|
|
98
|
+
groupName: string,
|
|
99
|
+
content: string,
|
|
100
|
+
type: TemplateStepGroupType | string,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
try {
|
|
103
|
+
await ensureStepsDirectory()
|
|
104
|
+
const formattedContent = await formatFileContent(content)
|
|
105
|
+
const filePath = getFilePath(groupName, type)
|
|
106
|
+
await fs.writeFile(filePath, formattedContent, 'utf8')
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`Failed to write template step file for group "${groupName}":`, error)
|
|
109
|
+
throw new Error(`File generation failed: ${error}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function deleteTemplateStepFile(groupName: string, type: TemplateStepGroupType | string): Promise<void> {
|
|
114
|
+
try {
|
|
115
|
+
const filePath = getFilePath(groupName, type)
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(filePath)
|
|
119
|
+
} catch {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await fs.unlink(filePath)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`Failed to delete template step file for group "${groupName}":`, error)
|
|
126
|
+
throw new Error(`File deletion failed: ${error}`)
|
|
127
|
+
}
|
|
128
|
+
}
|