create-appraisejs 0.2.0-alpha.5 → 0.2.0

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 CHANGED
@@ -1,52 +1,102 @@
1
- # create-appraisejs
2
-
3
- Scaffold a new [Appraise](https://github.com/jamil2018/appraisejs-core) app in your directory.
4
-
5
- ## Usage
6
-
7
- ```bash
8
- npx create-appraisejs@latest
9
- ```
10
-
11
- The CLI will prompt you for:
12
-
13
- 1. **Project directory** - Where to create the app (default: `./my-appraisejs-app`). The directory must be empty or not exist.
14
- 2. **Package manager** - `npm`, `pnpm`, `yarn`, or `bun`.
15
- 3. **Run production setup now** - Whether to install dependencies, create `.env`, and build the production app immediately.
16
- 4. **Playwright browsers** - Optional multi-select for `chromium`, `firefox`, and `webkit`.
17
-
18
- ## Default workflow
19
-
20
- 1. Uses the **bundled template** shipped with `create-appraisejs`.
21
- 2. Copies the template into the target directory.
22
- 3. Patches `package.json` scripts to use your selected package manager.
23
- 4. If you choose setup now, runs the project's `setup` script and then installs any selected Playwright browsers.
24
- 5. Prints the project path and production-first next steps (`setup`, optional `install-playwright`, then `start`).
25
-
26
- By default, the CLI does **not** download the repo again. Remote download is only used when you explicitly override the template source via environment variables.
27
-
28
- ## Template source and environment variables
29
-
30
- | Variable | Description | Default |
31
- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
32
- | `CREATE_APPRAISE_REPO_URL` | Git repository base URL (e.g. `https://github.com/jamil2018/appraisejs-core`). Used for both tarball and clone. | `https://github.com/jamil2018/appraisejs-core` |
33
- | `CREATE_APPRAISE_BRANCH` | Branch or ref to use. | `main` |
34
- | `CREATE_APPRAISE_TEMPLATE_SUBPATH` | Path inside the repo to the template directory (relative to repo root). | `templates/default` |
35
- | `CREATE_APPRAISE_USE_BUNDLED` | Set to `1`, `true`, or `yes` to force the bundled template even when remote overrides are present. | bundled template |
36
-
37
- When any remote override is provided, the CLI tries the GitHub tarball URL first (no git required); if that fails, it falls back to `git clone`.
38
-
39
- ## After scaffolding
40
-
41
- From the new project directory:
42
-
43
- - Run `npm run setup` (or your package manager's equivalent) to install dependencies, create `.env`, and build the local production app.
44
- - Optionally install Playwright browsers: `npm run install-playwright -- chromium firefox webkit`.
45
- - Start the production app: `npm run start`.
46
- - Use `npm run dev` only when you specifically want the development server.
47
-
48
- ## Recovery scripts
49
-
50
- - `npm run setup:db` recreates the local SQLite database from migrations and reruns `sync-all`.
51
- - `npm run setup:full` reruns dependency install, DB recovery, and the local production build.
52
- - `npm run appraisejs:sync` reruns the sync pipeline when you edit automation assets manually.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-appraisejs",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new AppraiseJS app in your directory",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hasnat Jamil",
@@ -1,5 +1,5 @@
1
1
  {
2
- "preparedAt": "2026-03-24T11:51:04.195Z",
3
- "inputHash": "596a28d2c5dc408d39926068711127354f3752a92c086cd0df27e8cf85eb5990",
2
+ "preparedAt": "2026-03-25T14:39:54.179Z",
3
+ "inputHash": "3d8e428a4dcbb25e993b9373c10b2a974387982a734dcfc9281c24bb38f6f36d",
4
4
  "databasePath": "prisma/dev.db"
5
5
  }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appraise",
3
- "version": "0.1.9-alpha",
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.1.9-alpha",
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>
@@ -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
- import Link from 'next/link'
45
- import NavMenuCardDeck from '@/components/navigation/nav-menu-card-deck'
46
- import NavCommand from '@/components/navigation/nav-command'
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 { FlaskConical } from 'lucide-react'
2
-
3
- const Logo = () => {
4
- return (
5
- <div className="m-2 flex items-center">
6
- <FlaskConical size={25} className="mr-2 text-primary" />
7
- <span className="rounded-l-lg bg-gray-800 bg-primary pb-1 pl-1 text-xl tracking-widest text-white underline dark:text-gray-700">
8
- app
9
- </span>
10
- <span className="text-xl tracking-widest text-primary">raise</span>
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
@@ -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
+ })
@@ -32,24 +32,24 @@ export function normalizeRoute(value: string | null | undefined): string {
32
32
  }
33
33
 
34
34
  function buildModulePathMap(modules: Module[]): Map<string, string> {
35
- const moduleById = new Map(modules.map(module => [module.id, module]))
35
+ const moduleById = new Map(modules.map(moduleRecord => [moduleRecord.id, moduleRecord]))
36
36
  const pathByModuleId = new Map<string, string>()
37
37
 
38
- const buildPath = (module: Module): string => {
39
- const cached = pathByModuleId.get(module.id)
38
+ const buildPath = (moduleRecord: Module): string => {
39
+ const cached = pathByModuleId.get(moduleRecord.id)
40
40
  if (cached) {
41
41
  return cached
42
42
  }
43
43
 
44
- const parent = module.parentId ? moduleById.get(module.parentId) : null
45
- const pathValue = parent ? `${buildPath(parent)}/${module.name}` : `/${module.name}`
44
+ const parent = moduleRecord.parentId ? moduleById.get(moduleRecord.parentId) : null
45
+ const pathValue = parent ? `${buildPath(parent)}/${moduleRecord.name}` : `/${moduleRecord.name}`
46
46
 
47
- pathByModuleId.set(module.id, pathValue.replace(/\/{2,}/g, '/'))
48
- return pathByModuleId.get(module.id)!
47
+ pathByModuleId.set(moduleRecord.id, pathValue.replace(/\/{2,}/g, '/'))
48
+ return pathByModuleId.get(moduleRecord.id)!
49
49
  }
50
50
 
51
- for (const module of modules) {
52
- buildPath(module)
51
+ for (const moduleRecord of modules) {
52
+ buildPath(moduleRecord)
53
53
  }
54
54
 
55
55
  return pathByModuleId
@@ -1,15 +1,15 @@
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'
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
13
  const REQUIRED_RUNTIME_IMPORT =
14
14
  `import { When, Then, CustomWorld, expect, SelectorName, resolveLocator, getEnvironment, generateRandomData, RandomDataType } from '${RUNTIME_IMPORT}';\n\n`
15
15
 
@@ -39,12 +39,12 @@ function generateStepDefinition(templateStep: TemplateStep): string | null {
39
39
 
40
40
  export function sanitizeFileName(groupName: string): string {
41
41
  return groupName
42
- .toLowerCase()
43
- .trim()
44
- .replace(/\s+/g, '_')
45
- .replace(/[^a-z0-9_]/g, '')
46
- }
47
-
42
+ .toLowerCase()
43
+ .trim()
44
+ .replace(/\s+/g, '_')
45
+ .replace(/[^a-z0-9_]/g, '')
46
+ }
47
+
48
48
  export function generateFileContent(templateSteps: TemplateStep[]): string {
49
49
  if (!templateSteps || templateSteps.length === 0) {
50
50
  return REQUIRED_RUNTIME_IMPORT + '// This file is generated automatically. Add template steps to this group to generate content.'
@@ -57,72 +57,72 @@ export function generateFileContent(templateSteps: TemplateStep[]): string {
57
57
 
58
58
  return REQUIRED_RUNTIME_IMPORT + functionDefinitions
59
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
- }
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
+ }