vyriy 0.3.5 → 0.3.8

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
@@ -36,6 +36,10 @@ vyriy init
36
36
  vyriy doctor
37
37
  vyriy --dry-run
38
38
  vyriy --yes
39
+ vyriy --no-install
40
+ vyriy --no-verify
41
+ vyriy --install-only
42
+ vyriy --verify
39
43
  vyriy --overwrite
40
44
  vyriy --skip-existing
41
45
  vyriy --help
@@ -48,7 +52,7 @@ Runs the same flow as `vyriy new`.
48
52
 
49
53
  ### `vyriy new [name]`
50
54
 
51
- Starts the project planning wizard, prints the project summary and file plan, then writes generated files when no unresolved conflicts exist.
55
+ Starts the project planning wizard, prints the project summary and file plan, writes generated files when no unresolved conflicts exist, installs dependencies, and runs generated project checks.
52
56
 
53
57
  If `name` is provided, it is used as the default project name and target directory.
54
58
 
@@ -79,11 +83,27 @@ Node.js is fatal when unsupported. Yarn and Git are warnings so generation can c
79
83
 
80
84
  ### `--dry-run`
81
85
 
82
- Prints the doctor report, project summary, and file plan without writing files or running fix commands.
86
+ Prints the doctor report, project summary, and file plan without writing files, installing dependencies, or running checks.
87
+
88
+ ### `--no-install`
89
+
90
+ Writes generated files but skips `yarn install` and `yarn check`.
91
+
92
+ ### `--no-verify`
93
+
94
+ Writes generated files and runs `yarn install`, but skips `yarn check`.
95
+
96
+ ### `--install-only`
97
+
98
+ Alias for `--no-verify`.
99
+
100
+ ### `--verify`
101
+
102
+ Explicitly enables `yarn check`. This is already the default unless `--no-install`, `--no-verify`, or `--install-only` is passed.
83
103
 
84
104
  ### `--yes`
85
105
 
86
- Uses default wizard answers and avoids prompts where possible. It does not overwrite existing files unless `--overwrite` is also passed.
106
+ Uses default wizard answers and avoids prompts where possible. In non-interactive mode, the default preset is `empty` with no CI/CD provider. It does not overwrite existing files unless `--overwrite` is also passed.
87
107
 
88
108
  ### `--overwrite`
89
109
 
@@ -106,10 +126,10 @@ The wizard collects:
106
126
  - project preset
107
127
  - API style for API-capable presets
108
128
  - CI/CD provider
109
- - optional infrastructure choices
129
+ - infrastructure provider
110
130
  - confirmation
111
131
 
112
- After confirmation, the CLI prints the project plan, creates generated files in memory, builds a conflict-aware file plan, and writes the accepted file plan.
132
+ After confirmation, the CLI prints the project plan, creates generated files in memory, builds a conflict-aware file plan, writes the accepted file plan, runs `yarn install`, and runs `yarn check`.
113
133
 
114
134
  Presets do not write to disk directly.
115
135
 
@@ -119,28 +139,28 @@ Generated projects always include `AGENTS.md` based on the shared Vyriy package
119
139
 
120
140
  Supported presets:
121
141
 
142
+ - `empty`
122
143
  - `library`
123
144
  - `api`
124
- - `react-csr`
125
- - `react-ssr`
126
- - `react-ssg`
127
- - `mfe`
128
- - `openmfe`
129
- - `mfe-bff`
130
- - `openmfe-bff`
145
+ - `ssr`
146
+ - `ssg`
147
+ - `csr`
131
148
  - `fullstack`
132
- - `aws-serverless`
133
- - `empty`
149
+ - `mfe`
150
+
151
+ The preset is the concrete future generated setup. The project kind is the broader architecture category. The infrastructure choice is selected separately: Docker is the default local/container shape, while AWS selects CDK plus Lambda/API Gateway for API-capable presets.
152
+
153
+ The `mfe` preset uses OpenMFE as the default MFE contract shape. There is no separate `openmfe` preset unless a future use case proves that split is useful.
134
154
 
135
- The preset is the concrete future generated setup. The project kind is the broader architecture category.
155
+ Workspace kinds describe deployment intent: `ui` is universal UI output, `api` is Docker-oriented, `lambda` is the AWS API runtime, `fargate` is an AWS container runtime, and `stack` contains AWS infrastructure.
136
156
 
137
157
  Examples:
138
158
 
139
- - `react-csr` -> `csr`
140
- - `react-ssr` -> `ssr`
141
- - `react-ssg` -> `ssg`
142
- - `openmfe-bff` -> `mfe`
143
- - `aws-serverless` -> `aws-serverless`
159
+ - `csr` -> `csr`
160
+ - `ssr` -> `ssr`
161
+ - `ssg` -> `ssg`
162
+ - `mfe` -> `mfe`
163
+ - `fullstack` -> `fullstack`
144
164
 
145
165
  ## Public API
146
166
 
@@ -178,7 +198,7 @@ It includes:
178
198
  - architecture: `preset`, `projectKind`
179
199
  - selected features
180
200
  - CI/CD planning: enabled state, providers, and validation pipelines
181
- - API planning for API-capable presets: REST, GraphQL, or mixed API style
201
+ - API planning for API-capable presets: REST or GraphQL API style
182
202
  - future package plans
183
203
  - future workspace plans
184
204
 
package/cli/args/args.js CHANGED
@@ -6,6 +6,8 @@ export const parseArgs = (args) => {
6
6
  return { type: 'version' };
7
7
  }
8
8
  const dryRun = args.includes('--dry-run');
9
+ const install = !args.includes('--no-install');
10
+ const verify = install && (args.includes('--verify') || (!args.includes('--no-verify') && !args.includes('--install-only')));
9
11
  const yes = args.includes('--yes') || args.includes('-y');
10
12
  const overwrite = args.includes('--overwrite');
11
13
  const skipExisting = args.includes('--skip-existing');
@@ -13,8 +15,10 @@ export const parseArgs = (args) => {
13
15
  const [command, projectName] = positionalArgs;
14
16
  const options = {
15
17
  dryRun,
18
+ install,
16
19
  yes,
17
20
  overwrite,
21
+ verify,
18
22
  skipExisting,
19
23
  };
20
24
  if (!command) {
@@ -2,14 +2,18 @@ export type VyriyCliCommand = {
2
2
  readonly type: 'new';
3
3
  readonly projectName?: string;
4
4
  readonly dryRun: boolean;
5
+ readonly install: boolean;
5
6
  readonly yes: boolean;
6
7
  readonly overwrite: boolean;
8
+ readonly verify: boolean;
7
9
  readonly skipExisting: boolean;
8
10
  } | {
9
11
  readonly type: 'init';
10
12
  readonly dryRun: boolean;
13
+ readonly install: boolean;
11
14
  readonly yes: boolean;
12
15
  readonly overwrite: boolean;
16
+ readonly verify: boolean;
13
17
  readonly skipExisting: boolean;
14
18
  } | {
15
19
  readonly type: 'doctor' | 'help' | 'version';
package/cli/cli.js CHANGED
@@ -10,9 +10,16 @@ Usage:
10
10
  vyriy init Initialize the current directory
11
11
  vyriy . Initialize the current directory
12
12
  vyriy doctor Check local environment
13
+ vyriy --yes, -y Use defaults where possible (empty preset)
13
14
  vyriy --dry-run Print checks and file plan without writing
14
- vyriy --help Show help
15
- vyriy --version Show version
15
+ vyriy --overwrite Overwrite existing generated paths
16
+ vyriy --skip-existing Leave existing generated paths untouched
17
+ vyriy --no-install Create files without installing dependencies
18
+ vyriy --no-verify Install dependencies without running checks
19
+ vyriy --install-only Alias for --no-verify
20
+ vyriy --verify Explicitly enable generated project checks
21
+ vyriy --help, -h Show help
22
+ vyriy --version, -v Show version
16
23
 
17
24
  Examples:
18
25
  vyriy new my-app
@@ -25,19 +32,23 @@ export const runVyriyCli = async (args = [], { output = console } = {}) => {
25
32
  case 'new':
26
33
  code = await runNewCommand({
27
34
  dryRun: command.dryRun,
35
+ install: command.install,
28
36
  output,
29
37
  overwrite: command.overwrite,
30
38
  projectName: command.projectName,
31
39
  skipExisting: command.skipExisting,
40
+ verify: command.verify,
32
41
  yes: command.yes,
33
42
  });
34
43
  break;
35
44
  case 'init':
36
45
  code = await runInitCommand({
37
46
  dryRun: command.dryRun,
47
+ install: command.install,
38
48
  output,
39
49
  overwrite: command.overwrite,
40
50
  skipExisting: command.skipExisting,
51
+ verify: command.verify,
41
52
  yes: command.yes,
42
53
  });
43
54
  break;
@@ -5,6 +5,8 @@ import { createFilePlan, printFilePlan, writeFilePlan } from '../../file-plan/in
5
5
  import { createProjectFiles } from '../../presets/index.js';
6
6
  import { askProjectPlan as askProjectPlanDefault } from '../../prompts/project-plan/index.js';
7
7
  import { createProjectPlanFromPreset, printProjectPlan } from '../../project-plan/index.js';
8
+ import { runCommand } from '../../shared/index.js';
9
+ const defaultYesPreset = 'empty';
8
10
  const getConflicts = (filePlan) => filePlan.filter((item) => item.status === 'conflict');
9
11
  const logConflicts = (output, conflicts, method) => {
10
12
  output[method]('\nExisting files found:\n');
@@ -28,6 +30,70 @@ const createResolvedFilePlan = async (plan, projectFiles, resolution) => createF
28
30
  overwrite: resolution === 'overwrite',
29
31
  skipExisting: resolution === 'skip',
30
32
  });
33
+ const formatCommand = (command, args) => [command, ...args].join(' ');
34
+ const printFailedPostGenerationCommand = ({ args, command, intro, output, projectDirectory, }) => {
35
+ const commandText = formatCommand(command, args);
36
+ output.error(`\n${intro}\n`);
37
+ output.error(`Failed command:\n ${commandText}\n`);
38
+ output.error(`Project directory:\n ${projectDirectory}\n`);
39
+ output.error(`You can inspect it and run manually:\n cd ${projectDirectory}\n ${commandText}`);
40
+ };
41
+ const runPostGenerationCommands = async ({ install, output, projectDirectory, verify, }) => {
42
+ if (!install) {
43
+ output.log('Installing dependencies... SKIPPED');
44
+ output.log('Running checks... SKIPPED');
45
+ output.log('\nProject files were created.');
46
+ return 0;
47
+ }
48
+ try {
49
+ await runCommand({
50
+ args: ['install'],
51
+ command: 'yarn',
52
+ cwd: projectDirectory,
53
+ });
54
+ }
55
+ catch {
56
+ printFailedPostGenerationCommand({
57
+ args: ['install'],
58
+ command: 'yarn',
59
+ intro: 'Project files were created, but dependency installation failed.',
60
+ output,
61
+ projectDirectory,
62
+ });
63
+ return 1;
64
+ }
65
+ output.log('Installing dependencies... OK');
66
+ if (!verify) {
67
+ output.log('Running checks... SKIPPED');
68
+ output.log('\nProject files were created and dependencies were installed.');
69
+ return 0;
70
+ }
71
+ try {
72
+ await runCommand({
73
+ args: ['fix'],
74
+ command: 'yarn',
75
+ cwd: projectDirectory,
76
+ });
77
+ await runCommand({
78
+ args: ['check'],
79
+ command: 'yarn',
80
+ cwd: projectDirectory,
81
+ });
82
+ }
83
+ catch {
84
+ printFailedPostGenerationCommand({
85
+ args: ['check'],
86
+ command: 'yarn',
87
+ intro: 'Project files were created and dependencies were installed, but verification failed.',
88
+ output,
89
+ projectDirectory,
90
+ });
91
+ return 1;
92
+ }
93
+ output.log('Running checks... OK');
94
+ output.log('\nProject is ready.');
95
+ return 0;
96
+ };
31
97
  const resolveInteractiveConflicts = async (plan, projectFiles, output, conflicts, askConflictResolution) => {
32
98
  logConflicts(output, conflicts, 'log');
33
99
  printConflictPrompt(output);
@@ -46,7 +112,7 @@ const resolveInteractiveConflicts = async (plan, projectFiles, output, conflicts
46
112
  export const askConflictResolutionDefault = async () => {
47
113
  const readline = createInterface({ input: stdin, output: stdout });
48
114
  try {
49
- const answer = (await readline.question('What should Vyriy do? 1. overwrite existing files, 2. skip existing files, 3. abort (abort): '))
115
+ const answer = (await readline.question('What should Vyriy do?\n\n 1. overwrite existing files,\n\n 2. skip existing files,\n\n 3. abort (abort): '))
50
116
  .trim()
51
117
  .toLowerCase();
52
118
  if (answer === '1' || answer === 'overwrite') {
@@ -61,7 +127,7 @@ export const askConflictResolutionDefault = async () => {
61
127
  readline.close();
62
128
  }
63
129
  };
64
- export const runNewCommand = async ({ askConflictResolution = askConflictResolutionDefault, askProjectPlan = askProjectPlanDefault, dryRun = false, output = console, overwrite = false, projectName = 'my-app', skipExisting = false, yes = false, } = {}) => {
130
+ export const runNewCommand = async ({ askConflictResolution = askConflictResolutionDefault, askProjectPlan = askProjectPlanDefault, dryRun = false, install = true, output = console, overwrite = false, projectName = 'my-app', skipExisting = false, verify = true, yes = false, } = {}) => {
65
131
  if (overwrite && skipExisting) {
66
132
  output.error('Cannot use --overwrite and --skip-existing together.');
67
133
  return 1;
@@ -72,13 +138,13 @@ export const runNewCommand = async ({ askConflictResolution = askConflictResolut
72
138
  output.error('\nPlease install Node.js 24+ and run the command again.');
73
139
  return 1;
74
140
  }
75
- const plan = dryRun || yes
141
+ const plan = yes
76
142
  ? createProjectPlanFromPreset({
77
143
  apiStyle: 'rest',
78
144
  ciProvider: 'none',
79
145
  description: 'Calm cloud-ready application.',
80
146
  packageScope: `@${projectName}`,
81
- preset: 'react-ssr',
147
+ preset: defaultYesPreset,
82
148
  projectName,
83
149
  targetDirectory: projectName,
84
150
  })
@@ -113,6 +179,11 @@ export const runNewCommand = async ({ askConflictResolution = askConflictResolut
113
179
  output.log(`\n${printFilePlan(resolved.filePlan)}`);
114
180
  }
115
181
  await writeFilePlan(plan.targetDirectory, filePlan);
116
- output.log('\nProject files written.');
117
- return 0;
182
+ output.log('\nCreating project files... OK');
183
+ return runPostGenerationCommands({
184
+ install,
185
+ output,
186
+ projectDirectory: plan.targetDirectory,
187
+ verify: install && verify,
188
+ });
118
189
  };
@@ -6,8 +6,10 @@ export type RunNewCommandOptions = {
6
6
  readonly askConflictResolution?: () => Promise<ConflictResolution>;
7
7
  readonly output?: Pick<typeof console, 'log' | 'error'>;
8
8
  readonly dryRun?: boolean;
9
+ readonly install?: boolean;
9
10
  readonly yes?: boolean;
10
11
  readonly overwrite?: boolean;
12
+ readonly verify?: boolean;
11
13
  readonly skipExisting?: boolean;
12
14
  };
13
15
  export type RunNewCommand = (options?: RunNewCommandOptions) => Promise<number>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vyriy",
3
- "version": "0.3.5",
3
+ "version": "0.3.8",
4
4
  "description": "Interactive project master for calm cloud-ready applications.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -489,6 +489,16 @@
489
489
  "import": "./shared/index.js",
490
490
  "default": "./shared/index.js"
491
491
  },
492
+ "./shared/runCommand": {
493
+ "types": "./shared/runCommand.d.ts",
494
+ "import": "./shared/runCommand.js",
495
+ "default": "./shared/runCommand.js"
496
+ },
497
+ "./shared/runCommand.js": {
498
+ "types": "./shared/runCommand.d.ts",
499
+ "import": "./shared/runCommand.js",
500
+ "default": "./shared/runCommand.js"
501
+ },
492
502
  "./shared/semver": {
493
503
  "types": "./shared/semver.d.ts",
494
504
  "import": "./shared/semver.js",
@@ -1,42 +1,94 @@
1
1
  import { agentsTemplate } from './agentsTemplate.js';
2
+ import rootPackageJson from '../../../package.json' with { type: 'json' };
2
3
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
3
- const createRootPackageJson = ({ description, packageScope, projectName, }) => ({
4
+ const packageVersion = (version) => `^${version}`;
5
+ const baseRootDevDependencies = {
6
+ '@vyriy/typescript-config': packageVersion(rootPackageJson.version),
7
+ typescript: rootPackageJson.dependencies.typescript,
8
+ '@vyriy/prettier-config': packageVersion(rootPackageJson.version),
9
+ prettier: rootPackageJson.dependencies.prettier,
10
+ '@vyriy/eslint-config': packageVersion(rootPackageJson.version),
11
+ eslint: rootPackageJson.dependencies.eslint,
12
+ '@vyriy/jest-config': packageVersion(rootPackageJson.version),
13
+ jest: rootPackageJson.dependencies.jest,
14
+ '@vyriy/storybook-config': packageVersion(rootPackageJson.version),
15
+ storybook: rootPackageJson.dependencies.storybook,
16
+ '@vyriy/path': packageVersion(rootPackageJson.version),
17
+ husky: rootPackageJson.dependencies.husky,
18
+ 'npm-run-all2': rootPackageJson.dependencies['npm-run-all2'],
19
+ 'cross-env': rootPackageJson.dependencies['cross-env'],
20
+ };
21
+ const stylelintDevDependencies = {
22
+ '@vyriy/stylelint-config': packageVersion(rootPackageJson.version),
23
+ stylelint: rootPackageJson.dependencies.stylelint,
24
+ };
25
+ const createRootPackageJson = ({ description, packageScope, projectName, stylelint, }) => ({
4
26
  path: 'package.json',
5
27
  content: json({
6
28
  name: `${packageScope}/${projectName}`,
7
- version: '0.1.0',
29
+ version: '0.0.0',
8
30
  description,
9
31
  private: true,
10
32
  type: 'module',
11
- packageManager: 'yarn@4.14.1',
33
+ packageManager: rootPackageJson.packageManager,
12
34
  engines: {
13
- node: '>=24.0.0',
35
+ node: rootPackageJson.engines.node,
14
36
  },
15
- scripts: {
16
- lint: 'eslint .',
17
- test: 'jest --coverage=false',
18
- build: 'tsc --pretty false',
19
- deploy: 'echo "Deploy is not configured yet."',
20
- smoke: 'echo "Smoke checks are not configured yet."',
21
- e2e: 'echo "E2E checks are not configured yet."',
22
- },
23
- devDependencies: {},
24
37
  workspaces: [
25
38
  'packages/*',
26
39
  'workspaces/*',
27
40
  ],
41
+ scripts: {
42
+ storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
43
+ check: 'run-s lint build test',
44
+ fix: "run-s 'fix:*'",
45
+ lint: "run-s 'lint:*'",
46
+ build: "run-s 'build:*'",
47
+ test: "run-s 'test:*'",
48
+ 'fix:prettier': 'prettier . --write',
49
+ 'fix:eslint': 'eslint . --fix',
50
+ 'lint:ts': 'tsc --pretty false',
51
+ 'lint:prettier': 'prettier . --check',
52
+ 'lint:eslint': 'eslint .',
53
+ ...(stylelint ? { 'lint:stylelint': 'stylelint "packages/**/*.{scss,css}"' } : {}),
54
+ 'build:dist': 'echo "Build dist is not configured yet."',
55
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
56
+ 'test:jest': 'jest --passWithNoTests',
57
+ postinstall: 'husky',
58
+ },
59
+ devDependencies: {
60
+ ...baseRootDevDependencies,
61
+ ...(stylelint ? stylelintDevDependencies : {}),
62
+ },
28
63
  }),
29
64
  });
30
65
  const createPackageManifest = ({ packageScope, workspaceName, }) => ({
31
66
  path: `packages/${workspaceName}/package.json`,
32
67
  content: json({
33
68
  name: `${packageScope}/${workspaceName}`,
34
- version: '0.1.0',
69
+ version: '0.0.0',
35
70
  private: true,
36
71
  type: 'module',
37
72
  main: 'index.js',
38
73
  }),
39
74
  });
75
+ const getWorkspacePath = (workspacePlan) => workspacePlan.kind === 'lambda' && workspacePlan.name === 'api'
76
+ ? `workspaces/lambda/${workspacePlan.name}`
77
+ : `workspaces/${workspacePlan.name}`;
78
+ const getWorkspacePackageName = ({ packageScope, workspacePlan, }) => workspacePlan.kind === 'lambda' && workspacePlan.name === 'api'
79
+ ? `${packageScope}/lambda-api-workspace`
80
+ : `${packageScope}/${workspacePlan.name}-workspace`;
81
+ const isApiWorkspace = (workspacePlan) => [
82
+ 'api',
83
+ 'lambda',
84
+ 'fargate',
85
+ ].includes(workspacePlan.kind);
86
+ const createApiWorkspaceDependencies = (workspacePlan) => isApiWorkspace(workspacePlan)
87
+ ? {
88
+ '@vyriy/handler': packageVersion(rootPackageJson.version),
89
+ '@vyriy/server': packageVersion(rootPackageJson.version),
90
+ }
91
+ : {};
40
92
  const createPackageFiles = (plan, packagePlan) => [
41
93
  createPackageManifest({
42
94
  packageScope: plan.packageScope,
@@ -72,39 +124,109 @@ const createPackageFiles = (plan, packagePlan) => [
72
124
  },
73
125
  ]),
74
126
  ];
75
- const createWorkspaceFiles = (plan, workspacePlan) => [
76
- {
77
- path: `workspaces/${workspacePlan.name}/package.json`,
78
- content: json({
79
- name: `${plan.packageScope}/${workspacePlan.name}-workspace`,
80
- version: '0.1.0',
81
- private: true,
82
- type: 'module',
83
- main: 'index.js',
84
- }),
85
- },
86
- {
87
- path: `workspaces/${workspacePlan.name}/index.ts`,
88
- content: 'export type WorkspaceName = string;\n',
89
- },
90
- {
91
- path: `workspaces/${workspacePlan.name}/${workspacePlan.name}.test.ts`,
92
- content: "import { describe, expect, it } from '@jest/globals';\n\ndescribe('workspace', () => {\n it('has a test harness', () => {\n expect(true).toBe(true);\n });\n});\n",
93
- },
94
- ];
127
+ const createApiHandlerFile = (workspacePath) => ({
128
+ path: `${workspacePath}/handler.ts`,
129
+ content: `import { api } from '@vyriy/handler';
130
+
131
+ export const handler = api(async (event) => ({
132
+ statusCode: 200,
133
+ body: JSON.stringify({
134
+ path: event.path,
135
+ }),
136
+ }));
137
+ `,
138
+ });
139
+ const createApiServerFile = ({ entrypoint, workspacePath, }) => ({
140
+ path: `${workspacePath}/${entrypoint}`,
141
+ content: `import { server } from '@vyriy/server';
142
+
143
+ import { handler } from './handler.js';
144
+
145
+ server(handler);
146
+ `,
147
+ });
148
+ const createDockerfile = (workspacePath) => ({
149
+ path: `${workspacePath}/Dockerfile`,
150
+ content: `FROM node:24-alpine
151
+
152
+ WORKDIR /app
153
+
154
+ COPY package.json yarn.lock .yarnrc.yml ./
155
+ COPY .yarn ./.yarn
156
+ COPY packages ./packages
157
+ COPY workspaces ./workspaces
158
+
159
+ RUN corepack enable && yarn install --immutable
160
+ RUN yarn build
161
+
162
+ CMD ["node", "workspaces/api/index.js"]
163
+ `,
164
+ });
165
+ const createApiWorkspaceFiles = (plan, workspacePlan) => {
166
+ const workspacePath = getWorkspacePath(workspacePlan);
167
+ const isLambda = workspacePlan.kind === 'lambda';
168
+ return [
169
+ createApiHandlerFile(workspacePath),
170
+ createApiServerFile({ entrypoint: isLambda ? 'server.ts' : 'index.ts', workspacePath }),
171
+ {
172
+ path: `${workspacePath}/${workspacePlan.name}.test.ts`,
173
+ content: "import { describe, expect, it } from '@jest/globals';\n\nimport { handler } from './handler.js';\n\ndescribe('api workspace', () => {\n it('exports a handler', () => {\n expect(handler).toEqual(expect.any(Function));\n });\n});\n",
174
+ },
175
+ ...(plan.features.includes('docker') && !isLambda ? [createDockerfile(workspacePath)] : []),
176
+ ];
177
+ };
178
+ const createWorkspaceFiles = (plan, workspacePlan) => {
179
+ const workspacePath = getWorkspacePath(workspacePlan);
180
+ const dependencies = createApiWorkspaceDependencies(workspacePlan);
181
+ return [
182
+ {
183
+ path: `${workspacePath}/package.json`,
184
+ content: json({
185
+ name: getWorkspacePackageName({ packageScope: plan.packageScope, workspacePlan }),
186
+ version: '0.0.0',
187
+ private: true,
188
+ type: 'module',
189
+ main: workspacePlan.kind === 'lambda' ? 'server.js' : 'index.js',
190
+ ...(Object.keys(dependencies).length > 0 ? { dependencies } : {}),
191
+ }),
192
+ },
193
+ ...(isApiWorkspace(workspacePlan)
194
+ ? createApiWorkspaceFiles(plan, workspacePlan)
195
+ : [
196
+ {
197
+ path: `${workspacePath}/index.ts`,
198
+ content: 'export type WorkspaceName = string;\n',
199
+ },
200
+ {
201
+ path: `${workspacePath}/${workspacePlan.name}.test.ts`,
202
+ content: "import { describe, expect, it } from '@jest/globals';\n\ndescribe('workspace', () => {\n it('has a test harness', () => {\n expect(true).toBe(true);\n });\n});\n",
203
+ },
204
+ ]),
205
+ ];
206
+ };
95
207
  const shouldCreateStylelintConfig = (plan) => plan.features.some((feature) => [
96
208
  'react',
97
209
  'webpack',
98
210
  ].includes(feature));
211
+ const getStylePackageName = (plan) => plan.packages.find((packagePlan) => packagePlan.kind === 'ui')?.name ?? 'ui';
99
212
  export const createProjectFiles = (plan) => [
100
- createRootPackageJson(plan),
213
+ createRootPackageJson({
214
+ ...plan,
215
+ stylelint: shouldCreateStylelintConfig(plan),
216
+ }),
101
217
  {
102
218
  path: 'README.md',
103
219
  content: `# ${plan.projectName}\n\n${plan.description}\n`,
104
220
  },
105
221
  {
106
222
  path: 'doc.mdx',
107
- content: "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Project/README\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
223
+ content: `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
224
+ import ReadMe from './README.md?raw';
225
+
226
+ <Meta title="${plan.projectName}" />
227
+
228
+ <Markdown>{ReadMe}</Markdown>
229
+ `,
108
230
  },
109
231
  {
110
232
  path: 'AGENTS.md',
@@ -112,11 +234,64 @@ export const createProjectFiles = (plan) => [
112
234
  },
113
235
  {
114
236
  path: '.editorconfig',
115
- content: 'root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n',
237
+ content: `# https://editorconfig.org
238
+ root = true
239
+
240
+ [*]
241
+ charset = utf-8
242
+ end_of_line = lf
243
+ insert_final_newline = true
244
+ trim_trailing_whitespace = true
245
+
246
+ indent_style = space
247
+ indent_size = 2
248
+
249
+ max_line_length = 100
250
+
251
+ # Markdown
252
+ [*.md]
253
+ trim_trailing_whitespace = false
254
+ max_line_length = off
255
+
256
+ # YAML / YML
257
+ [*.{yml,yaml}]
258
+ indent_size = 2
259
+
260
+ # JSON
261
+ [*.json]
262
+ indent_size = 2
263
+
264
+ # TypeScript / JavaScript
265
+ [*.{ts,tsx,js,jsx}]
266
+ indent_size = 2
267
+
268
+ # Shell / Bash
269
+ [*.sh]
270
+ indent_size = 2`,
116
271
  },
117
272
  {
118
273
  path: '.gitignore',
119
- content: 'node_modules/\ndist/\ncoverage/\n.yarn/cache/\n.env\n',
274
+ content: `.yarn/*
275
+ !.yarn/cache
276
+ !.yarn/patches
277
+ !.yarn/plugins
278
+ !.yarn/releases
279
+ !.yarn/sdks
280
+ !.yarn/versions
281
+
282
+ .DS_Store
283
+ .idea
284
+ node_modules
285
+ coverage
286
+ dist
287
+ storybook-static
288
+ *storybook.log
289
+ consumer
290
+
291
+ cdk.out
292
+ cdk.context.json
293
+
294
+ !/**/.gitkeep`,
120
295
  },
121
296
  {
122
297
  path: '.npmrc',
@@ -124,12 +299,50 @@ export const createProjectFiles = (plan) => [
124
299
  },
125
300
  {
126
301
  path: '.nvmrc',
127
- content: '24\n',
302
+ content: 'lts/krypton',
128
303
  },
129
304
  {
130
305
  path: '.yarnrc.yml',
131
306
  content: 'nodeLinker: node-modules\n',
132
307
  },
308
+ {
309
+ path: '.husky/commit-msg',
310
+ content: '#!/bin/sh\n',
311
+ },
312
+ {
313
+ path: '.husky/post-checkout',
314
+ content: '#!/bin/sh\n\nyarn\n',
315
+ },
316
+ {
317
+ path: '.husky/post-merge',
318
+ content: '#!/bin/sh\n\nyarn\n',
319
+ },
320
+ {
321
+ path: '.husky/pre-commit',
322
+ content: '#!/bin/sh\n\nyarn check\n',
323
+ },
324
+ {
325
+ path: '.husky/pre-push',
326
+ content: '#!/bin/sh\n\nyarn check\n',
327
+ },
328
+ {
329
+ path: '.storybook/main.ts',
330
+ content: `import config from '@vyriy/storybook-config';
331
+ import { path } from '@vyriy/path';
332
+
333
+ export default {
334
+ ...config,
335
+ stories: [
336
+ path('**/*.mdx'),
337
+ path('**/*.stories.@(js|jsx|mjs|ts|tsx)'),
338
+ ],
339
+ };
340
+ `,
341
+ },
342
+ {
343
+ path: '.storybook/preview.tsx',
344
+ content: "export { default } from '@vyriy/storybook-config/preview';\n",
345
+ },
133
346
  {
134
347
  path: 'yarn.lock',
135
348
  content: '',
@@ -138,10 +351,9 @@ export const createProjectFiles = (plan) => [
138
351
  path: 'tsconfig.json',
139
352
  content: json({
140
353
  extends: '@vyriy/typescript-config/index.json',
141
- compilerOptions: {
142
- noEmit: false,
143
- },
144
354
  include: [
355
+ '.storybook/**/*.ts',
356
+ '.storybook/**/*.tsx',
145
357
  'packages/**/*.ts',
146
358
  'packages/**/*.tsx',
147
359
  'workspaces/**/*.ts',
@@ -172,6 +384,23 @@ export const createProjectFiles = (plan) => [
172
384
  path: 'stylelint.config.ts',
173
385
  content: "export { default } from '@vyriy/stylelint-config';\n",
174
386
  },
387
+ {
388
+ path: `packages/${getStylePackageName(plan)}/reset.scss`,
389
+ content: `html {
390
+ box-sizing: border-box;
391
+ }
392
+
393
+ *,
394
+ *::before,
395
+ *::after {
396
+ box-sizing: inherit;
397
+ }
398
+
399
+ body {
400
+ margin: 0;
401
+ }
402
+ `,
403
+ },
175
404
  ]
176
405
  : []),
177
406
  ...plan.packages.flatMap((packagePlan) => createPackageFiles(plan, packagePlan)),
@@ -1,30 +1,25 @@
1
1
  const apiPresets = [
2
2
  'api',
3
3
  'fullstack',
4
- 'mfe-bff',
5
- 'openmfe-bff',
6
- 'aws-serverless',
4
+ 'mfe',
7
5
  ];
8
6
  export const isApiPreset = (preset) => apiPresets.includes(preset);
9
- export const getApiRuntimeFromPreset = (preset) => preset === 'aws-serverless' ? 'lambda' : 'node';
10
- export const getDefaultApiStyleFromPreset = (preset) => preset === 'openmfe-bff' ? 'mixed' : 'rest';
7
+ export const getApiRuntimeFromPreset = () => 'docker';
8
+ export const getDefaultApiStyleFromPreset = () => 'rest';
11
9
  export const getFeaturesFromApiPlan = (api) => {
12
10
  if (!api) {
13
11
  return [];
14
12
  }
15
- if (api.style === 'mixed') {
16
- return ['rest-api', 'graphql-api'];
17
- }
18
13
  return api.style === 'rest' ? ['rest-api'] : ['graphql-api'];
19
14
  };
20
- export const createApiPlan = ({ preset, style = getDefaultApiStyleFromPreset(preset) }) => {
15
+ export const createApiPlan = ({ preset, runtime = getApiRuntimeFromPreset(preset), style = getDefaultApiStyleFromPreset(preset), }) => {
21
16
  if (!isApiPreset(preset)) {
22
17
  return undefined;
23
18
  }
24
19
  const basePlan = {
25
20
  enabled: true,
26
21
  style,
27
- runtime: getApiRuntimeFromPreset(preset),
22
+ runtime,
28
23
  };
29
24
  const restPlan = {
30
25
  rest: {
@@ -37,13 +32,6 @@ export const createApiPlan = ({ preset, style = getDefaultApiStyleFromPreset(pre
37
32
  packageName: 'graphql',
38
33
  },
39
34
  };
40
- if (style === 'mixed') {
41
- return {
42
- ...basePlan,
43
- ...restPlan,
44
- ...graphqlPlan,
45
- };
46
- }
47
35
  return style === 'rest'
48
36
  ? {
49
37
  ...basePlan,
@@ -1,6 +1,7 @@
1
1
  import { VyriyApiPlan, VyriyApiRuntime, VyriyApiStyle, VyriyFeature, VyriyPreset } from '../types.js';
2
2
  export type CreateApiPlanOptions = {
3
3
  readonly preset: VyriyPreset;
4
+ readonly runtime?: VyriyApiRuntime;
4
5
  readonly style?: VyriyApiStyle;
5
6
  };
6
7
  export type CreateApiPlan = (options: CreateApiPlanOptions) => VyriyApiPlan | undefined;
@@ -9,113 +9,104 @@ const baseFeatures = [
9
9
  'storybook',
10
10
  ];
11
11
  const presetFeatures = {
12
+ empty: [],
12
13
  library: ['react'],
13
14
  api: [],
14
- 'react-csr': ['react', 'webpack'],
15
- 'react-ssr': ['react', 'webpack'],
16
- 'react-ssg': ['react', 'webpack'],
17
- mfe: ['react', 'webpack'],
18
- openmfe: ['react', 'webpack', 'openmfe'],
19
- 'mfe-bff': ['react', 'webpack', 'bff'],
20
- 'openmfe-bff': [
21
- 'react',
22
- 'webpack',
23
- 'openmfe',
24
- 'bff',
25
- ],
15
+ csr: ['react', 'webpack'],
16
+ ssr: ['react', 'webpack'],
17
+ ssg: ['react', 'webpack'],
26
18
  fullstack: ['react', 'webpack'],
27
- 'aws-serverless': [
28
- 'aws-cdk',
29
- 'lambda',
30
- 'apigateway',
31
- ],
32
- empty: [],
19
+ mfe: ['react', 'webpack', 'openmfe'],
33
20
  };
34
21
  const packagePlans = {
22
+ empty: [],
35
23
  library: [{ name: 'ui', kind: 'ui', publishable: true }],
36
24
  api: [{ name: 'api', kind: 'api', publishable: false }],
37
- 'react-csr': [
38
- { name: 'app', kind: 'core', publishable: false },
39
- { name: 'ui', kind: 'ui', publishable: true },
40
- ],
41
- 'react-ssr': [
42
- { name: 'app', kind: 'core', publishable: false },
43
- { name: 'ui', kind: 'ui', publishable: true },
44
- { name: 'ssr', kind: 'ssr', publishable: false },
45
- ],
46
- 'react-ssg': [
47
- { name: 'app', kind: 'core', publishable: false },
48
- { name: 'ui', kind: 'ui', publishable: true },
49
- { name: 'ssg', kind: 'ssg', publishable: false },
50
- { name: 'content', kind: 'core', publishable: false },
51
- ],
52
- mfe: [
53
- { name: 'mfe', kind: 'mfe', publishable: false },
54
- { name: 'ui', kind: 'ui', publishable: true },
55
- ],
56
- openmfe: [
57
- { name: 'mfe', kind: 'mfe', publishable: false },
25
+ csr: [
26
+ { name: 'app', kind: 'app', publishable: false },
58
27
  { name: 'ui', kind: 'ui', publishable: true },
59
- { name: 'openmfe-contract', kind: 'contract', publishable: true },
60
28
  ],
61
- 'mfe-bff': [
62
- { name: 'mfe', kind: 'mfe', publishable: false },
29
+ ssr: [
30
+ { name: 'app', kind: 'app', publishable: false },
63
31
  { name: 'ui', kind: 'ui', publishable: true },
64
- { name: 'bff', kind: 'bff', publishable: false },
32
+ { name: 'ssr', kind: 'app', publishable: false },
65
33
  ],
66
- 'openmfe-bff': [
67
- { name: 'mfe', kind: 'mfe', publishable: false },
34
+ ssg: [
35
+ { name: 'app', kind: 'app', publishable: false },
68
36
  { name: 'ui', kind: 'ui', publishable: true },
69
- { name: 'bff', kind: 'bff', publishable: false },
70
- { name: 'openmfe-contract', kind: 'contract', publishable: true },
37
+ { name: 'ssg', kind: 'app', publishable: false },
38
+ { name: 'content', kind: 'utils', publishable: false },
71
39
  ],
72
40
  fullstack: [
73
- { name: 'app', kind: 'core', publishable: false },
41
+ { name: 'app', kind: 'app', publishable: false },
74
42
  { name: 'ui', kind: 'ui', publishable: true },
75
43
  { name: 'api', kind: 'api', publishable: false },
76
44
  ],
77
- 'aws-serverless': [
45
+ mfe: [
46
+ { name: 'mfe', kind: 'app', publishable: false },
47
+ { name: 'ui', kind: 'ui', publishable: true },
78
48
  { name: 'api', kind: 'api', publishable: false },
79
- { name: 'stack', kind: 'stack', publishable: false },
49
+ { name: 'openmfe-contract', kind: 'config', publishable: true },
80
50
  ],
81
- empty: [],
82
51
  };
83
52
  const workspacePlans = {
53
+ empty: [],
84
54
  library: [],
85
55
  api: [{ name: 'api', kind: 'api' }],
86
- 'react-csr': [{ name: 'web', kind: 'web' }],
87
- 'react-ssr': [
88
- { name: 'ssr', kind: 'ssr' },
89
- { name: 'web', kind: 'web' },
90
- ],
91
- 'react-ssg': [
92
- { name: 'ssg', kind: 'ssg' },
93
- { name: 'web', kind: 'web' },
94
- ],
95
- mfe: [{ name: 'mfe', kind: 'mfe' }],
96
- openmfe: [
97
- { name: 'mfe', kind: 'mfe' },
98
- { name: 'openmfe', kind: 'openmfe' },
99
- ],
100
- 'mfe-bff': [
101
- { name: 'mfe', kind: 'mfe' },
102
- { name: 'bff', kind: 'bff' },
56
+ csr: [{ name: 'web', kind: 'ui' }],
57
+ ssr: [
58
+ { name: 'ssr', kind: 'ui' },
59
+ { name: 'web', kind: 'ui' },
103
60
  ],
104
- 'openmfe-bff': [
105
- { name: 'mfe', kind: 'mfe' },
106
- { name: 'bff', kind: 'bff' },
107
- { name: 'openmfe', kind: 'openmfe' },
61
+ ssg: [
62
+ { name: 'ssg', kind: 'ui' },
63
+ { name: 'web', kind: 'ui' },
108
64
  ],
109
65
  fullstack: [
110
- { name: 'web', kind: 'web' },
66
+ { name: 'web', kind: 'ui' },
67
+ { name: 'api', kind: 'api' },
68
+ ],
69
+ mfe: [
70
+ { name: 'mfe', kind: 'ui' },
111
71
  { name: 'api', kind: 'api' },
112
72
  ],
113
- 'aws-serverless': [{ name: 'stack', kind: 'stack' }],
114
- empty: [],
115
73
  };
116
74
  const uniqueFeatures = (features) => [...new Set(features)];
75
+ const getApiRuntimeFromFeatures = (features) => features.includes('lambda') ? 'lambda' : 'docker';
76
+ const awsInfrastructureFeatures = [
77
+ 'aws-cdk',
78
+ 'lambda',
79
+ 'fargate',
80
+ 's3',
81
+ 'cloudfront',
82
+ ];
83
+ const getApiWorkspaceKindFromFeatures = (features) => {
84
+ if (features.includes('lambda')) {
85
+ return 'lambda';
86
+ }
87
+ if (features.includes('fargate')) {
88
+ return 'fargate';
89
+ }
90
+ return 'api';
91
+ };
92
+ const createWorkspacePlans = ({ features, preset, }) => {
93
+ const apiWorkspaceKind = getApiWorkspaceKindFromFeatures(features);
94
+ const workspaces = workspacePlans[preset].map((workspacePlan) => workspacePlan.name === 'api'
95
+ ? {
96
+ ...workspacePlan,
97
+ kind: apiWorkspaceKind,
98
+ }
99
+ : workspacePlan);
100
+ if (features.some((feature) => awsInfrastructureFeatures.includes(feature))) {
101
+ return [
102
+ ...workspaces,
103
+ { name: 'stack', kind: 'stack' },
104
+ ];
105
+ }
106
+ return workspaces;
107
+ };
117
108
  export const createProjectPlanFromPreset = ({ description, apiStyle, ciProvider, features = [], packageScope, preset, projectName, targetDirectory, }) => {
118
- const api = createApiPlan({ preset, style: apiStyle });
109
+ const api = createApiPlan({ preset, runtime: getApiRuntimeFromFeatures(features), style: apiStyle });
119
110
  const apiFeatures = getFeaturesFromApiPlan(api);
120
111
  return {
121
112
  projectName,
@@ -131,7 +122,7 @@ export const createProjectPlanFromPreset = ({ description, apiStyle, ciProvider,
131
122
  ...features,
132
123
  ]),
133
124
  packages: [...packagePlans[preset]],
134
- workspaces: [...workspacePlans[preset]],
125
+ workspaces: createWorkspacePlans({ features, preset }),
135
126
  ci: createCiPlan({ provider: ciProvider }),
136
127
  ...(api ? { api } : {}),
137
128
  };
@@ -1,16 +1 @@
1
- export const getProjectKindFromPreset = (preset) => {
2
- switch (preset) {
3
- case 'react-csr':
4
- return 'csr';
5
- case 'react-ssr':
6
- return 'ssr';
7
- case 'react-ssg':
8
- return 'ssg';
9
- case 'openmfe':
10
- case 'mfe-bff':
11
- case 'openmfe-bff':
12
- return 'mfe';
13
- default:
14
- return preset;
15
- }
16
- };
1
+ export const getProjectKindFromPreset = (preset) => preset;
@@ -1,14 +1,14 @@
1
- export type VyriyProjectKind = 'library' | 'api' | 'csr' | 'ssr' | 'ssg' | 'mfe' | 'fullstack' | 'aws-serverless' | 'empty';
2
- export type VyriyPreset = 'library' | 'api' | 'react-csr' | 'react-ssr' | 'react-ssg' | 'mfe' | 'openmfe' | 'mfe-bff' | 'openmfe-bff' | 'fullstack' | 'aws-serverless' | 'empty';
3
- export type VyriyFeature = 'typescript' | 'eslint' | 'prettier' | 'jest' | 'rest-api' | 'graphql-api' | 'react' | 'storybook' | 'webpack' | 'docker' | 'aws-cdk' | 'apigateway' | 'lambda' | 'fargate' | 's3' | 'cloudfront' | 'openmfe' | 'bff';
1
+ export type VyriyProjectKind = 'library' | 'api' | 'csr' | 'ssr' | 'ssg' | 'mfe' | 'fullstack' | 'empty';
2
+ export type VyriyPreset = 'empty' | 'library' | 'api' | 'ssr' | 'ssg' | 'csr' | 'fullstack' | 'mfe';
3
+ export type VyriyFeature = 'typescript' | 'eslint' | 'prettier' | 'jest' | 'rest-api' | 'graphql-api' | 'react' | 'storybook' | 'webpack' | 'docker' | 'aws-cdk' | 'apigateway' | 'lambda' | 'fargate' | 's3' | 'cloudfront' | 'openmfe';
4
4
  export type VyriyPackagePlan = {
5
5
  readonly name: string;
6
- readonly kind: 'core' | 'ui' | 'api' | 'bff' | 'ssr' | 'ssg' | 'mfe' | 'contract' | 'stack';
6
+ readonly kind: 'core' | 'ui' | 'api' | 'services' | 'stack' | 'config' | 'utils' | 'components' | 'app';
7
7
  readonly publishable: boolean;
8
8
  };
9
9
  export type VyriyWorkspacePlan = {
10
10
  readonly name: string;
11
- readonly kind: 'web' | 'api' | 'ssr' | 'ssg' | 'storybook' | 'bff' | 'mfe' | 'openmfe' | 'stack';
11
+ readonly kind: 'api' | 'ui' | 'stack' | 'lambda' | 'fargate';
12
12
  };
13
13
  export type VyriyCiProvider = 'gitlab' | 'github';
14
14
  export type VyriyCiPipeline = 'install' | 'lint' | 'test' | 'build' | 'deploy' | 'smoke' | 'e2e';
@@ -17,8 +17,8 @@ export type VyriyCiPlan = {
17
17
  readonly providers: VyriyCiProvider[];
18
18
  readonly pipelines: VyriyCiPipeline[];
19
19
  };
20
- export type VyriyApiStyle = 'rest' | 'graphql' | 'mixed';
21
- export type VyriyApiRuntime = 'node' | 'lambda';
20
+ export type VyriyApiStyle = 'rest' | 'graphql';
21
+ export type VyriyApiRuntime = 'docker' | 'lambda';
22
22
  export type VyriyApiPlan = {
23
23
  readonly enabled: boolean;
24
24
  readonly style: VyriyApiStyle;
@@ -2,25 +2,34 @@ import { stdin, stdout } from 'node:process';
2
2
  import { createInterface } from 'node:readline';
3
3
  import { createProjectPlanFromPreset, getDefaultApiStyleFromPreset, isApiPreset, } from '../../project-plan/index.js';
4
4
  const presets = [
5
+ 'empty',
5
6
  'library',
6
7
  'api',
7
- 'react-csr',
8
- 'react-ssr',
9
- 'react-ssg',
10
- 'mfe',
11
- 'openmfe',
12
- 'mfe-bff',
13
- 'openmfe-bff',
8
+ 'ssr',
9
+ 'ssg',
10
+ 'csr',
14
11
  'fullstack',
15
- 'aws-serverless',
16
- 'empty',
17
- ];
18
- const extraFeatures = [
19
- 'docker',
20
- 'aws-api',
21
- 'aws-fargate',
22
- 'aws-static',
12
+ 'mfe',
23
13
  ];
14
+ const presetDescriptions = {
15
+ empty: 'shared tooling without application code',
16
+ library: 'publishable React package for reusable UI',
17
+ api: 'REST or GraphQL backend API',
18
+ ssr: 'server-rendered React application',
19
+ ssg: 'build-time generated static React site',
20
+ csr: 'browser-rendered React application',
21
+ fullstack: 'React frontend with backend API',
22
+ mfe: 'OpenMFE widget with UI, API, SSR, and manifest',
23
+ };
24
+ const infrastructureOptions = ['docker', 'aws'];
25
+ const infrastructureFeatureMap = {
26
+ docker: ['docker'],
27
+ aws: [
28
+ 'aws-cdk',
29
+ 'lambda',
30
+ 'apigateway',
31
+ ],
32
+ };
24
33
  const extraFeatureMap = {
25
34
  docker: ['docker'],
26
35
  'aws-api': [
@@ -48,7 +57,7 @@ const directFeatureInputs = [
48
57
  's3',
49
58
  'cloudfront',
50
59
  ];
51
- const apiStyles = ['rest', 'graphql', 'mixed'];
60
+ const apiStyles = ['rest', 'graphql'];
52
61
  const ciProviders = ['none', 'gitlab', 'github'];
53
62
  const createQuestion = (readline, output) => {
54
63
  const queuedLines = [];
@@ -96,6 +105,28 @@ const parseFeatures = (value) => [
96
105
  ? [feature]
97
106
  : []))),
98
107
  ];
108
+ const parseInfrastructure = (value, defaultValue) => {
109
+ const normalizedValue = value.trim().toLowerCase();
110
+ const numericValue = Number.parseInt(normalizedValue, 10);
111
+ if (Number.isInteger(numericValue) && infrastructureOptions[numericValue - 1]) {
112
+ return [...infrastructureFeatureMap[infrastructureOptions[numericValue - 1]]];
113
+ }
114
+ if (infrastructureOptions.includes(normalizedValue)) {
115
+ return [...infrastructureFeatureMap[normalizedValue]];
116
+ }
117
+ const legacyFeatures = parseFeatures(value);
118
+ return legacyFeatures.length > 0 ? legacyFeatures : [...infrastructureFeatureMap[defaultValue]];
119
+ };
120
+ const getDefaultInfrastructureInput = (features) => features?.some((feature) => [
121
+ 'aws-cdk',
122
+ 'lambda',
123
+ 'apigateway',
124
+ 'fargate',
125
+ 's3',
126
+ 'cloudfront',
127
+ ].includes(feature))
128
+ ? 'aws'
129
+ : 'docker';
99
130
  const parseApiStyle = (value, defaultValue) => {
100
131
  const normalizedValue = value.trim().toLowerCase();
101
132
  const numericValue = Number.parseInt(normalizedValue, 10);
@@ -124,12 +155,13 @@ export const askProjectPlan = async ({ defaults = {}, input = stdin, output = st
124
155
  const packageScope = await promptWithDefault(question, 'Package scope', defaults.packageScope ?? `@${projectName}`);
125
156
  const description = await promptWithDefault(question, 'Description', defaults.description ?? 'Calm cloud-ready application.');
126
157
  output.write('\nProject preset:\n');
127
- presets.forEach((preset, index) => output.write(` ${index + 1}. ${preset}\n`));
128
- const presetAnswer = await promptWithDefault(question, 'Preset number or name', defaults.preset ?? 'react-ssr');
129
- const preset = parsePreset(presetAnswer, defaults.preset ?? 'react-ssr');
158
+ presets.forEach((preset, index) => output.write(` ${index + 1}. ${preset} - ${presetDescriptions[preset]}\n`));
159
+ const defaultPreset = defaults.preset ?? 'empty';
160
+ const presetAnswer = await promptWithDefault(question, 'Preset number or name', defaultPreset);
161
+ const preset = parsePreset(presetAnswer, defaultPreset);
130
162
  const defaultApiStyle = defaults.apiStyle ?? getDefaultApiStyleFromPreset(preset);
131
163
  const apiStyle = isApiPreset(preset)
132
- ? parseApiStyle(await promptWithDefault(question, 'API style: 1. rest (@vyriy/router), 2. graphql, 3. mixed', defaultApiStyle), defaultApiStyle)
164
+ ? parseApiStyle(await promptWithDefault(question, 'API style:\n 1. rest (@vyriy/router),\n 2. graphql', defaultApiStyle), defaultApiStyle)
133
165
  : undefined;
134
166
  output.write('\nCI/CD provider:\n');
135
167
  output.write(' 1. none\n');
@@ -137,10 +169,12 @@ export const askProjectPlan = async ({ defaults = {}, input = stdin, output = st
137
169
  output.write(' 3. github\n');
138
170
  const defaultCiProvider = defaults.ciProvider ?? 'none';
139
171
  const ciProvider = parseCiProvider(await promptWithDefault(question, 'CI/CD provider number or name', defaultCiProvider), defaultCiProvider);
140
- output.write('\nAdditional infrastructure, comma-separated:\n');
141
- output.write(` ${extraFeatures.join(', ')}\n`);
142
- const featuresAnswer = await promptWithDefault(question, 'Infrastructure', defaults.features?.join(', ') ?? '');
143
- const features = parseFeatures(featuresAnswer);
172
+ output.write('\nInfrastructure:\n');
173
+ output.write(' 1. Docker\n');
174
+ output.write(' 2. AWS\n');
175
+ const defaultInfrastructure = getDefaultInfrastructureInput(defaults.features);
176
+ const featuresAnswer = await promptWithDefault(question, 'Infrastructure number or name', defaultInfrastructure);
177
+ const features = parseInfrastructure(featuresAnswer, defaultInfrastructure);
144
178
  const plan = createProjectPlanFromPreset({
145
179
  projectName,
146
180
  targetDirectory,
package/shared/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './commandExists.js';
2
2
  export * from './execCommand.js';
3
3
  export * from './fileExists.js';
4
+ export * from './runCommand.js';
4
5
  export * from './semver.js';
5
6
  export type * from './types.js';
package/shared/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './commandExists.js';
2
2
  export * from './execCommand.js';
3
3
  export * from './fileExists.js';
4
+ export * from './runCommand.js';
4
5
  export * from './semver.js';
@@ -0,0 +1,9 @@
1
+ import { RunCommand, RunCommandOptions } from './types.js';
2
+ export declare class RunCommandError extends Error {
3
+ readonly args: readonly string[];
4
+ readonly command: string;
5
+ readonly cwd: string;
6
+ readonly exitCode: number | null;
7
+ constructor({ args, command, cwd }: RunCommandOptions, exitCode: number | null);
8
+ }
9
+ export declare const runCommand: RunCommand;
@@ -0,0 +1,34 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { env as processEnv } from 'node:process';
3
+ export class RunCommandError extends Error {
4
+ args;
5
+ command;
6
+ cwd;
7
+ exitCode;
8
+ constructor({ args = [], command, cwd }, exitCode) {
9
+ super(`Command failed with exit code ${exitCode ?? 'unknown'}: ${command} ${args.join(' ')}`.trim());
10
+ this.name = 'RunCommandError';
11
+ this.args = args;
12
+ this.command = command;
13
+ this.cwd = cwd;
14
+ this.exitCode = exitCode;
15
+ }
16
+ }
17
+ export const runCommand = async (options) => {
18
+ const child = spawn(options.command, [...(options.args ?? [])], {
19
+ cwd: options.cwd,
20
+ env: options.env ?? processEnv,
21
+ shell: false,
22
+ stdio: 'inherit',
23
+ });
24
+ await new Promise((resolve, reject) => {
25
+ child.on('error', reject);
26
+ child.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve();
29
+ return;
30
+ }
31
+ reject(new RunCommandError(options, code));
32
+ });
33
+ });
34
+ };
package/shared/types.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export type ExecCommand = (command: string, args?: readonly string[]) => Promise<string>;
2
+ export type RunCommandOptions = {
3
+ readonly command: string;
4
+ readonly args?: readonly string[];
5
+ readonly cwd: string;
6
+ readonly env?: Record<string, string | undefined>;
7
+ };
8
+ export type RunCommand = (options: RunCommandOptions) => Promise<void>;
2
9
  export type CommandExists = (command: string, options?: {
3
10
  readonly execCommand?: ExecCommand;
4
11
  }) => Promise<boolean>;