create-nyoworks 2.7.2 → 3.1.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.
@@ -0,0 +1,45 @@
1
+ export interface AppDefinition {
2
+ id: string;
3
+ name: string;
4
+ name_tr: string;
5
+ description: string;
6
+ description_tr: string;
7
+ platforms: ("web" | "mobile" | "desktop")[];
8
+ features: string[];
9
+ integrations: string[];
10
+ }
11
+ export interface IntegrationDefinition {
12
+ name: string;
13
+ providers?: string[];
14
+ default?: string;
15
+ services?: string[];
16
+ free_tier?: Record<string, string>;
17
+ note?: string;
18
+ }
19
+ export interface PlatformDefinition {
20
+ name: string;
21
+ framework: string;
22
+ version: string;
23
+ }
24
+ export interface AppsConfig {
25
+ apps: AppDefinition[];
26
+ integrations: Record<string, IntegrationDefinition>;
27
+ platforms: Record<string, PlatformDefinition>;
28
+ }
29
+ export declare function loadAppsConfig(repoDir: string): Promise<AppsConfig>;
30
+ export declare function appToPromptChoice(app: AppDefinition, lang?: string): {
31
+ title: string;
32
+ value: string;
33
+ description: string;
34
+ };
35
+ export declare function integrationToPromptChoices(integration: IntegrationDefinition, integrationId: string): {
36
+ choices: {
37
+ title: string;
38
+ value: string;
39
+ }[];
40
+ initial: number;
41
+ message: string;
42
+ } | null;
43
+ export declare function getAppFeatures(app: AppDefinition): string[];
44
+ export declare function getAppIntegrations(app: AppDefinition): string[];
45
+ export declare function getAppPlatforms(app: AppDefinition): string[];
@@ -0,0 +1,68 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // Config Loader - Load app definitions from YAML
3
+ // ═══════════════════════════════════════════════════════════════════════════════
4
+ import fs from "fs-extra";
5
+ import path from "path";
6
+ import { parse } from "yaml";
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ // Loader Functions
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ export async function loadAppsConfig(repoDir) {
11
+ const configPath = path.join(repoDir, "config", "apps.yaml");
12
+ if (!await fs.pathExists(configPath)) {
13
+ throw new Error("config/apps.yaml not found in repository");
14
+ }
15
+ const content = await fs.readFile(configPath, "utf8");
16
+ return parse(content);
17
+ }
18
+ export function appToPromptChoice(app, lang = "en") {
19
+ const name = lang === "tr" ? app.name_tr : app.name;
20
+ const desc = lang === "tr" ? app.description_tr : app.description;
21
+ return {
22
+ title: name,
23
+ value: app.id,
24
+ description: desc,
25
+ };
26
+ }
27
+ export function integrationToPromptChoices(integration, integrationId) {
28
+ if (!integration.providers || integration.providers.length <= 1) {
29
+ return null;
30
+ }
31
+ const defaultIndex = integration.providers.indexOf(integration.default || integration.providers[0]);
32
+ return {
33
+ choices: integration.providers.map((provider) => ({
34
+ title: formatProviderName(provider),
35
+ value: provider,
36
+ })),
37
+ initial: defaultIndex >= 0 ? defaultIndex : 0,
38
+ message: `${integration.name} provider:`,
39
+ };
40
+ }
41
+ function formatProviderName(provider) {
42
+ const names = {
43
+ mollie: "Mollie (iDEAL, Bancontact)",
44
+ adyen: "Adyen",
45
+ stripe: "Stripe",
46
+ resend: "Resend (3000/ay free)",
47
+ brevo: "Brevo (300/gün free)",
48
+ twilio: "Twilio",
49
+ plivo: "Plivo",
50
+ postnl: "PostNL",
51
+ sendcloud: "Sendcloud",
52
+ dhl: "DHL",
53
+ storecove: "Storecove",
54
+ };
55
+ return names[provider] || provider.charAt(0).toUpperCase() + provider.slice(1);
56
+ }
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ // Feature Helpers
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+ export function getAppFeatures(app) {
61
+ return app.features || [];
62
+ }
63
+ export function getAppIntegrations(app) {
64
+ return app.integrations || [];
65
+ }
66
+ export function getAppPlatforms(app) {
67
+ return app.platforms || ["web"];
68
+ }
package/dist/init.js CHANGED
@@ -1,47 +1,23 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // Create NYOWORKS - Project Initialization (Multi-Product Support)
3
+ // ═══════════════════════════════════════════════════════════════════════════════
1
4
  import prompts from "prompts";
2
5
  import pc from "picocolors";
3
6
  import fs from "fs-extra";
4
7
  import path from "path";
5
8
  import os from "os";
9
+ import { fileURLToPath } from "url";
6
10
  import { execa } from "execa";
7
11
  import { replacePlaceholders } from "./replace.js";
8
12
  import { checkDependencies, showClaudeMaxWarning, getDockerComposeCommand } from "./checks.js";
13
+ import { loadAppsConfig, appToPromptChoice, integrationToPromptChoices, getAppFeatures, getAppPlatforms, } from "./config-loader.js";
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Constants
18
+ // ─────────────────────────────────────────────────────────────────────────────
9
19
  const REPO = "naimozcan/nyoworks-framework";
10
20
  const BRANCH = "main";
11
- const PRODUCT_TYPES = [
12
- { title: "E-Commerce", value: "ecommerce", description: "Online store, product sales" },
13
- { title: "Booking", value: "booking", description: "Appointment/reservation system" },
14
- { title: "SaaS Platform", value: "saas", description: "Subscription-based software" },
15
- { title: "Marketplace", value: "marketplace", description: "Multi-vendor platform" },
16
- { title: "Content/Blog", value: "content", description: "Blog, news, CMS" },
17
- { title: "CRM", value: "crm", description: "Customer relationship management" },
18
- { title: "Custom", value: "custom", description: "Manual feature selection" },
19
- ];
20
- const REQUIRED_FEATURES = {
21
- ecommerce: ["payments", "crm", "notifications", "search", "storage"],
22
- booking: ["appointments", "payments", "crm", "notifications"],
23
- saas: ["payments", "subscriptions", "analytics", "notifications", "audit", "multitenant"],
24
- marketplace: ["payments", "crm", "analytics", "notifications", "audit", "search", "storage", "multitenant"],
25
- content: ["analytics", "search", "storage"],
26
- crm: ["crm", "analytics", "notifications", "audit", "export"],
27
- custom: [],
28
- };
29
- const AVAILABLE_FEATURES = [
30
- { title: "Analytics", value: "analytics", description: "User behavior tracking" },
31
- { title: "Appointments", value: "appointments", description: "Booking system" },
32
- { title: "Audit", value: "audit", description: "Activity logging" },
33
- { title: "Auth Social", value: "auth-social", description: "Google, Apple, GitHub OAuth" },
34
- { title: "CRM", value: "crm", description: "Customer relationship management" },
35
- { title: "Export", value: "export", description: "PDF/CSV export" },
36
- { title: "i18n", value: "i18n", description: "Multi-language support" },
37
- { title: "Multitenant", value: "multitenant", description: "Multi-organization support" },
38
- { title: "Notifications", value: "notifications", description: "Email, SMS, Push" },
39
- { title: "Payments", value: "payments", description: "Stripe integration" },
40
- { title: "Realtime", value: "realtime", description: "WebSocket support" },
41
- { title: "Search", value: "search", description: "Full-text search" },
42
- { title: "Storage", value: "storage", description: "File uploads (S3/R2)" },
43
- { title: "Subscriptions", value: "subscriptions", description: "Plans & usage limits" },
44
- ];
45
21
  const AVAILABLE_PLATFORMS = [
46
22
  { title: "Web", value: "web", description: "Next.js 16" },
47
23
  { title: "Mobile", value: "mobile", description: "Expo SDK 54" },
@@ -57,6 +33,16 @@ const LANGUAGE_RESPONSES = {
57
33
  en: "English",
58
34
  nl: "Dutch",
59
35
  };
36
+ const ADDITIONAL_FEATURES = [
37
+ { title: "Realtime", value: "realtime", description: "WebSocket support" },
38
+ { title: "i18n", value: "i18n", description: "Multi-language support" },
39
+ { title: "Auth Social", value: "auth-social", description: "Google, Apple, GitHub OAuth" },
40
+ { title: "Multitenant", value: "multitenant", description: "Multi-organization support" },
41
+ { title: "Subscriptions", value: "subscriptions", description: "Plans & usage limits" },
42
+ ];
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Helper Functions
45
+ // ─────────────────────────────────────────────────────────────────────────────
60
46
  function generateCode(name) {
61
47
  return name
62
48
  .toUpperCase()
@@ -84,13 +70,44 @@ async function downloadRepo(repo, branch) {
84
70
  await execa("tar", ["-xzf", tarFile, "-C", tempDir]);
85
71
  return path.join(tempDir, `nyoworks-framework-${branch}`);
86
72
  }
73
+ function getLocalRepoPath() {
74
+ const localPath = path.resolve(__dirname, "..", "..", "..");
75
+ const configPath = path.join(localPath, "config", "apps.yaml");
76
+ if (fs.existsSync(configPath)) {
77
+ return localPath;
78
+ }
79
+ return null;
80
+ }
81
+ async function replacePackageNames(targetDir, slug) {
82
+ const pkgFiles = await findPackageJsonFiles(targetDir);
83
+ for (const pkgFile of pkgFiles) {
84
+ let content = await fs.readFile(pkgFile, "utf8");
85
+ content = content.replace(/@nyoworks\//g, `@${slug}/`);
86
+ await fs.writeFile(pkgFile, content);
87
+ }
88
+ }
89
+ async function findPackageJsonFiles(dir) {
90
+ const results = [];
91
+ const entries = await fs.readdir(dir, { withFileTypes: true });
92
+ for (const entry of entries) {
93
+ const fullPath = path.join(dir, entry.name);
94
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
95
+ results.push(...await findPackageJsonFiles(fullPath));
96
+ }
97
+ else if (entry.isFile() && entry.name === "package.json") {
98
+ results.push(fullPath);
99
+ }
100
+ }
101
+ return results;
102
+ }
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+ // Main Function
105
+ // ─────────────────────────────────────────────────────────────────────────────
87
106
  export async function createProject(projectName) {
88
107
  console.log();
89
108
  console.log(pc.cyan(pc.bold(" NYOWORKS Framework")));
90
- console.log(pc.dim(" Create a new project"));
109
+ console.log(pc.dim(" Create a new multi-product project"));
91
110
  console.log();
92
- let productType = "custom";
93
- let requiredFeatures = [];
94
111
  const nameResponse = await prompts({
95
112
  type: projectName ? null : "text",
96
113
  name: "name",
@@ -102,79 +119,175 @@ export async function createProject(projectName) {
102
119
  console.log(pc.red("Aborted."));
103
120
  process.exit(1);
104
121
  }
105
- const productResponse = await prompts({
106
- type: "select",
107
- name: "productType",
108
- message: "Product type:",
109
- choices: PRODUCT_TYPES,
110
- initial: 0,
111
- });
112
- if (productResponse.productType) {
113
- productType = productResponse.productType;
114
- requiredFeatures = REQUIRED_FEATURES[productType] || [];
115
- }
116
- const featureChoices = AVAILABLE_FEATURES.map((f) => {
117
- const isRequired = requiredFeatures.includes(f.value);
118
- return {
119
- title: isRequired ? `${f.title} [required]` : f.title,
120
- value: f.value,
121
- description: f.description,
122
- selected: isRequired,
123
- };
124
- });
125
- const response = await prompts([
126
- {
127
- type: "multiselect",
128
- name: "platforms",
129
- message: "Select platforms:",
130
- choices: AVAILABLE_PLATFORMS,
131
- min: 1,
132
- hint: "- Space to select. Return to submit",
133
- instructions: false,
134
- },
135
- {
136
- type: "multiselect",
137
- name: "features",
138
- message: "Select features:",
139
- choices: featureChoices,
140
- hint: "- Space to select. Return to submit",
141
- instructions: false,
142
- },
143
- {
144
- type: "select",
145
- name: "language",
146
- message: "Agent response language:",
147
- choices: AVAILABLE_LANGUAGES,
148
- initial: 0,
149
- },
150
- ]);
151
122
  const name = (nameResponse.name || projectName);
152
123
  const code = generateCode(name);
153
124
  const slug = generateSlug(name);
154
125
  const databaseName = generateDatabaseName(name);
155
- const platforms = response.platforms || ["web"];
156
- const selectedFeatures = response.features || [];
157
- const features = [...new Set([...requiredFeatures, ...selectedFeatures])];
158
- const language = response.language || "tr";
159
126
  const targetDir = path.resolve(process.cwd(), slug);
160
127
  if (fs.existsSync(targetDir)) {
161
128
  console.log(pc.red(`Directory ${slug} already exists.`));
162
129
  process.exit(1);
163
130
  }
164
131
  console.log();
165
- process.stdout.write(pc.cyan("Downloading from GitHub..."));
166
132
  let repoDir;
167
- try {
168
- repoDir = await downloadRepo(REPO, BRANCH);
169
- console.log(pc.green(" done"));
133
+ let config;
134
+ let isLocalMode = false;
135
+ const localRepo = getLocalRepoPath();
136
+ if (localRepo) {
137
+ process.stdout.write(pc.cyan("Using local framework..."));
138
+ repoDir = localRepo;
139
+ isLocalMode = true;
140
+ try {
141
+ config = await loadAppsConfig(repoDir);
142
+ console.log(pc.green(" done"));
143
+ }
144
+ catch (error) {
145
+ console.log(pc.red(" failed"));
146
+ console.error(pc.red("Failed to load local config."));
147
+ process.exit(1);
148
+ }
170
149
  }
171
- catch (error) {
172
- console.log(pc.red(" failed"));
173
- console.error(pc.red("Failed to download from GitHub. Check your internet connection."));
150
+ else {
151
+ process.stdout.write(pc.cyan("Downloading from GitHub..."));
152
+ try {
153
+ repoDir = await downloadRepo(REPO, BRANCH);
154
+ config = await loadAppsConfig(repoDir);
155
+ console.log(pc.green(" done"));
156
+ }
157
+ catch (error) {
158
+ console.log(pc.red(" failed"));
159
+ console.error(pc.red("Failed to download from GitHub. Check your internet connection."));
160
+ process.exit(1);
161
+ }
162
+ }
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+ // Multi-Product Selection
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+ const appChoices = config.apps.map((app) => ({
167
+ ...appToPromptChoice(app, "tr"),
168
+ selected: false,
169
+ }));
170
+ const productsResponse = await prompts({
171
+ type: "multiselect",
172
+ name: "appIds",
173
+ message: "Products (birden fazla seçebilirsiniz):",
174
+ choices: appChoices,
175
+ min: 1,
176
+ hint: "- Space to select. Return to submit",
177
+ instructions: false,
178
+ });
179
+ if (!productsResponse.appIds || productsResponse.appIds.length === 0) {
180
+ console.log(pc.red("Aborted."));
174
181
  process.exit(1);
175
182
  }
183
+ const selectedAppIds = productsResponse.appIds;
184
+ const selectedApps = config.apps.filter((a) => selectedAppIds.includes(a.id));
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+ // Per-Product Platform Selection
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+ const productSelections = [];
189
+ for (const app of selectedApps) {
190
+ console.log();
191
+ console.log(pc.cyan(` ${app.name_tr || app.name} platformları:`));
192
+ const appPlatforms = getAppPlatforms(app);
193
+ const platformChoices = AVAILABLE_PLATFORMS
194
+ .filter((p) => appPlatforms.includes(p.value))
195
+ .map((p) => ({ ...p, selected: p.value === "web" }));
196
+ const platformResponse = await prompts({
197
+ type: "multiselect",
198
+ name: "platforms",
199
+ message: `${app.id} platforms:`,
200
+ choices: platformChoices,
201
+ min: 1,
202
+ hint: "- Space to select. Return to submit",
203
+ instructions: false,
204
+ });
205
+ const platforms = (platformResponse.platforms || ["web"]);
206
+ productSelections.push({ app, platforms });
207
+ }
208
+ // ─────────────────────────────────────────────────────────────────────────────
209
+ // Collect All Features from All Products
210
+ // ─────────────────────────────────────────────────────────────────────────────
211
+ const allAppFeatures = new Set();
212
+ const allIntegrations = new Set();
213
+ for (const selection of productSelections) {
214
+ const features = getAppFeatures(selection.app);
215
+ features.forEach((f) => allAppFeatures.add(f));
216
+ for (const integration of selection.app.integrations || []) {
217
+ allIntegrations.add(integration);
218
+ }
219
+ }
220
+ // ─────────────────────────────────────────────────────────────────────────────
221
+ // Integration Provider Selection (Once for All Products)
222
+ // ─────────────────────────────────────────────────────────────────────────────
223
+ const selectedProviders = {};
224
+ for (const integrationId of allIntegrations) {
225
+ const integration = config.integrations?.[integrationId];
226
+ if (!integration)
227
+ continue;
228
+ const promptConfig = integrationToPromptChoices(integration, integrationId);
229
+ if (!promptConfig) {
230
+ selectedProviders[integrationId] = integration.default || integration.providers?.[0] || integrationId;
231
+ continue;
232
+ }
233
+ const providerResponse = await prompts({
234
+ type: "select",
235
+ name: "provider",
236
+ message: promptConfig.message,
237
+ choices: promptConfig.choices,
238
+ initial: promptConfig.initial,
239
+ });
240
+ if (providerResponse.provider) {
241
+ selectedProviders[integrationId] = providerResponse.provider;
242
+ }
243
+ }
244
+ // ─────────────────────────────────────────────────────────────────────────────
245
+ // Additional Features
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ const additionalFeaturesNotInApps = ADDITIONAL_FEATURES.filter((f) => !allAppFeatures.has(f.value));
248
+ let additionalFeatures = [];
249
+ if (additionalFeaturesNotInApps.length > 0) {
250
+ const additionalResponse = await prompts({
251
+ type: "multiselect",
252
+ name: "features",
253
+ message: "Ek feature'lar (isteğe bağlı):",
254
+ choices: additionalFeaturesNotInApps,
255
+ hint: "- Space to select. Return to submit",
256
+ instructions: false,
257
+ });
258
+ additionalFeatures = additionalResponse.features || [];
259
+ }
260
+ // ─────────────────────────────────────────────────────────────────────────────
261
+ // Language Selection
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+ const languageResponse = await prompts({
264
+ type: "select",
265
+ name: "language",
266
+ message: "Agent language:",
267
+ choices: AVAILABLE_LANGUAGES,
268
+ initial: 0,
269
+ });
270
+ const language = languageResponse.language || "tr";
271
+ const features = [...new Set([...allAppFeatures, ...additionalFeatures])];
272
+ // ─────────────────────────────────────────────────────────────────────────────
273
+ // Build Project Config
274
+ // ─────────────────────────────────────────────────────────────────────────────
275
+ const projectConfig = {
276
+ name,
277
+ code,
278
+ slug,
279
+ databaseName,
280
+ products: productSelections,
281
+ features,
282
+ providers: selectedProviders,
283
+ language,
284
+ };
285
+ console.log();
176
286
  process.stdout.write(pc.dim(" Copying files..."));
177
287
  await fs.ensureDir(targetDir);
288
+ // ─────────────────────────────────────────────────────────────────────────────
289
+ // Copy Core Packages (Shared)
290
+ // ─────────────────────────────────────────────────────────────────────────────
178
291
  const corePaths = [
179
292
  "packages/api",
180
293
  "packages/api-client",
@@ -187,6 +300,7 @@ export async function createProject(projectName) {
187
300
  "docs/bible",
188
301
  "mcp-server",
189
302
  ".claude",
303
+ "config",
190
304
  ];
191
305
  for (const p of corePaths) {
192
306
  const src = path.join(repoDir, p);
@@ -195,13 +309,32 @@ export async function createProject(projectName) {
195
309
  await fs.copy(src, dest);
196
310
  }
197
311
  }
198
- for (const platform of platforms) {
199
- const src = path.join(repoDir, `apps/${platform}`);
200
- const dest = path.join(targetDir, `apps/${platform}`);
201
- if (await fs.pathExists(src)) {
202
- await fs.copy(src, dest);
312
+ // ─────────────────────────────────────────────────────────────────────────────
313
+ // Copy Apps (Per Product + Platform)
314
+ // ─────────────────────────────────────────────────────────────────────────────
315
+ for (const selection of productSelections) {
316
+ const appId = selection.app.id;
317
+ for (const platform of selection.platforms) {
318
+ const templateSrc = path.join(repoDir, `apps/_templates/${platform}`);
319
+ const legacySrc = path.join(repoDir, `apps/${platform}`);
320
+ const dest = path.join(targetDir, `apps/${appId}/${platform}`);
321
+ if (await fs.pathExists(templateSrc)) {
322
+ await fs.copy(templateSrc, dest);
323
+ }
324
+ else if (await fs.pathExists(legacySrc)) {
325
+ await fs.copy(legacySrc, dest);
326
+ }
327
+ const pkgPath = path.join(dest, "package.json");
328
+ if (await fs.pathExists(pkgPath)) {
329
+ const pkg = await fs.readJson(pkgPath);
330
+ pkg.name = `@${slug}/${appId}-${platform}`;
331
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
332
+ }
203
333
  }
204
334
  }
335
+ // ─────────────────────────────────────────────────────────────────────────────
336
+ // Copy Feature Packages
337
+ // ─────────────────────────────────────────────────────────────────────────────
205
338
  for (const feature of features) {
206
339
  const src = path.join(repoDir, `packages/features/${feature}`);
207
340
  const dest = path.join(targetDir, `packages/features/${feature}`);
@@ -209,6 +342,29 @@ export async function createProject(projectName) {
209
342
  await fs.copy(src, dest);
210
343
  }
211
344
  }
345
+ const integrationFeatureMap = {
346
+ payments: ["payments"],
347
+ shipping: ["shipping"],
348
+ invoicing: ["invoicing"],
349
+ whatsapp: ["whatsapp"],
350
+ google: ["google"],
351
+ ai: ["ai"],
352
+ email: [],
353
+ sms: [],
354
+ };
355
+ for (const integrationId of allIntegrations) {
356
+ const featuresToCopy = integrationFeatureMap[integrationId] || [];
357
+ for (const featureName of featuresToCopy) {
358
+ const src = path.join(repoDir, `packages/features/${featureName}`);
359
+ const dest = path.join(targetDir, `packages/features/${featureName}`);
360
+ if (await fs.pathExists(src) && !await fs.pathExists(dest)) {
361
+ await fs.copy(src, dest);
362
+ }
363
+ }
364
+ }
365
+ // ─────────────────────────────────────────────────────────────────────────────
366
+ // Copy Root Files
367
+ // ─────────────────────────────────────────────────────────────────────────────
212
368
  const rootFiles = [
213
369
  "package.json",
214
370
  "pnpm-workspace.yaml",
@@ -227,22 +383,41 @@ export async function createProject(projectName) {
227
383
  await fs.copy(src, dest);
228
384
  }
229
385
  }
230
- await fs.remove(path.dirname(repoDir));
386
+ if (!isLocalMode) {
387
+ await fs.remove(path.dirname(repoDir));
388
+ }
231
389
  console.log(pc.green(" done"));
390
+ // ─────────────────────────────────────────────────────────────────────────────
391
+ // Update Package Names
392
+ // ─────────────────────────────────────────────────────────────────────────────
393
+ process.stdout.write(pc.dim(" Updating package names..."));
394
+ await replacePackageNames(targetDir, slug);
395
+ console.log(pc.green(" done"));
396
+ // ─────────────────────────────────────────────────────────────────────────────
397
+ // Replace Placeholders
398
+ // ─────────────────────────────────────────────────────────────────────────────
399
+ const primaryApp = productSelections[0].app;
400
+ const allPlatforms = [...new Set(productSelections.flatMap((s) => s.platforms))];
232
401
  const placeholders = {
233
402
  "${PROJECT_NAME}": name,
234
403
  "${PROJECT_CODE}": code,
235
404
  "${PROJECT_SLUG}": slug,
236
405
  "${DATABASE_NAME}": databaseName,
237
- "${PRODUCT_TYPE}": productType,
406
+ "${APP_ID}": primaryApp.id,
407
+ "${APP_NAME}": primaryApp.name,
408
+ "${PRODUCT_TYPE}": primaryApp.id,
238
409
  "${RESPONSE_LANGUAGE}": LANGUAGE_RESPONSES[language] || "Turkish",
239
410
  };
240
411
  process.stdout.write(pc.dim(" Replacing placeholders..."));
241
412
  await replacePlaceholders(targetDir, placeholders);
242
413
  console.log(pc.green(" done"));
414
+ // ─────────────────────────────────────────────────────────────────────────────
415
+ // Create Feature Docs
416
+ // ─────────────────────────────────────────────────────────────────────────────
243
417
  for (const feature of features) {
244
418
  const featureDoc = path.join(targetDir, "docs", "bible", "features", `${feature}.md`);
245
- const content = `# Feature: ${feature.charAt(0).toUpperCase() + feature.slice(1)}
419
+ if (!await fs.pathExists(featureDoc)) {
420
+ const content = `# Feature: ${feature.charAt(0).toUpperCase() + feature.slice(1)}
246
421
 
247
422
  ## Overview
248
423
 
@@ -275,20 +450,48 @@ See \`docs/bible/data/schema.md\`
275
450
  |----|----------|-----------|
276
451
  | T-xxx | [Decision] | [Why] |
277
452
  `;
278
- await fs.outputFile(featureDoc, content);
279
- console.log(pc.dim(` Created docs/bible/features/${feature}.md`));
453
+ await fs.outputFile(featureDoc, content);
454
+ console.log(pc.dim(` Created docs/bible/features/${feature}.md`));
455
+ }
280
456
  }
457
+ // ─────────────────────────────────────────────────────────────────────────────
458
+ // Generate Multi-Product Config (apps.config.yaml)
459
+ // ─────────────────────────────────────────────────────────────────────────────
460
+ const appsConfigContent = generateAppsConfigYaml(projectConfig);
461
+ await fs.outputFile(path.join(targetDir, "config", "apps.config.yaml"), appsConfigContent);
462
+ console.log(pc.dim(" Created config/apps.config.yaml (multi-product config)"));
463
+ // ─────────────────────────────────────────────────────────────────────────────
464
+ // Update nyoworks.config.yaml
465
+ // ─────────────────────────────────────────────────────────────────────────────
281
466
  const configPath = path.join(targetDir, "nyoworks.config.yaml");
282
467
  if (await fs.pathExists(configPath)) {
283
- let config = await fs.readFile(configPath, "utf8");
468
+ let configContent = await fs.readFile(configPath, "utf8");
284
469
  if (features.length > 0) {
285
- config = config.replace(/enabled: \[\]/, `enabled:\n${features.map((f) => ` - ${f}`).join("\n")}`);
470
+ configContent = configContent.replace(/enabled: \[\]/, `enabled:\n${features.map((f) => ` - ${f}`).join("\n")}`);
286
471
  }
287
- config = config.replace(/targets:\n - web/, `targets:\n${platforms.map((p) => ` - ${p}`).join("\n")}`);
288
- await fs.writeFile(configPath, config);
472
+ configContent = configContent.replace(/targets:\n - web/, `targets:\n${allPlatforms.map((p) => ` - ${p}`).join("\n")}`);
473
+ await fs.writeFile(configPath, configContent);
474
+ }
475
+ // ─────────────────────────────────────────────────────────────────────────────
476
+ // Update pnpm-workspace.yaml
477
+ // ─────────────────────────────────────────────────────────────────────────────
478
+ const workspacePath = path.join(targetDir, "pnpm-workspace.yaml");
479
+ if (await fs.pathExists(workspacePath)) {
480
+ const workspaceContent = `packages:
481
+ - "apps/server"
482
+ - "apps/*/*"
483
+ - "packages/*"
484
+ - "packages/features/*"
485
+ - "packages/platforms/*"
486
+ - "mcp-server"
487
+ `;
488
+ await fs.writeFile(workspacePath, workspaceContent);
289
489
  }
490
+ // ─────────────────────────────────────────────────────────────────────────────
491
+ // Success Message
492
+ // ─────────────────────────────────────────────────────────────────────────────
290
493
  console.log();
291
- console.log(pc.green(pc.bold("Project created successfully!")));
494
+ console.log(pc.green(pc.bold("Multi-product project created successfully!")));
292
495
  await checkDependencies();
293
496
  const dockerCmd = await getDockerComposeCommand();
294
497
  showClaudeMaxWarning();
@@ -307,11 +510,19 @@ See \`docs/bible/data/schema.md\`
307
510
  console.log(pc.dim(" Configuration:"));
308
511
  console.log(pc.dim(` Name: ${name}`));
309
512
  console.log(pc.dim(` Code: ${code}`));
310
- console.log(pc.dim(` Product: ${productType}`));
311
- console.log(pc.dim(` Platforms: ${platforms.join(", ")}`));
513
+ console.log(pc.dim(" Products:"));
514
+ for (const selection of productSelections) {
515
+ console.log(pc.dim(` - ${selection.app.name} (${selection.app.id}): ${selection.platforms.join(", ")}`));
516
+ }
312
517
  console.log(pc.dim(` Features: ${features.join(", ") || "none"}`));
518
+ if (Object.keys(selectedProviders).length > 0) {
519
+ console.log(pc.dim(` Providers: ${Object.entries(selectedProviders).map(([k, v]) => `${k}:${v}`).join(", ")}`));
520
+ }
313
521
  console.log(pc.dim(` Language: ${LANGUAGE_RESPONSES[language] || "Turkish"}`));
314
522
  console.log();
523
+ // ─────────────────────────────────────────────────────────────────────────────
524
+ // Build MCP Server
525
+ // ─────────────────────────────────────────────────────────────────────────────
315
526
  process.stdout.write(pc.dim(" Building MCP server..."));
316
527
  const mcpPath = path.join(targetDir, "mcp-server");
317
528
  try {
@@ -341,3 +552,59 @@ See \`docs/bible/data/schema.md\`
341
552
  await fs.outputJson(mcpConfigPath, mcpConfig, { spaces: 2 });
342
553
  console.log(pc.dim(" Created .claude/settings.local.json"));
343
554
  }
555
+ // ─────────────────────────────────────────────────────────────────────────────
556
+ // Generate Multi-Product Config YAML
557
+ // ─────────────────────────────────────────────────────────────────────────────
558
+ function generateAppsConfigYaml(config) {
559
+ const productsYaml = config.products.map((p) => ` ${p.app.id}:
560
+ name: "${p.app.name}"
561
+ name_tr: "${p.app.name_tr || p.app.name}"
562
+ platforms:
563
+ ${p.platforms.map((pl) => ` - ${pl}`).join("\n")}
564
+ features:
565
+ ${getAppFeatures(p.app).map((f) => ` - ${f}`).join("\n")}`).join("\n\n");
566
+ const providersYaml = Object.entries(config.providers)
567
+ .map(([k, v]) => ` ${k}: "${v}"`)
568
+ .join("\n");
569
+ return `# ═══════════════════════════════════════════════════════════════════════════════
570
+ # ${config.name} - Multi-Product Configuration
571
+ # ═══════════════════════════════════════════════════════════════════════════════
572
+
573
+ project:
574
+ name: "${config.name}"
575
+ code: "${config.code}"
576
+ slug: "${config.slug}"
577
+ database: "${config.databaseName}"
578
+ language: "${config.language}"
579
+
580
+ # ─────────────────────────────────────────────────────────────────────────────
581
+ # Products (Apps)
582
+ # ─────────────────────────────────────────────────────────────────────────────
583
+
584
+ products:
585
+ ${productsYaml}
586
+
587
+ # ─────────────────────────────────────────────────────────────────────────────
588
+ # Shared Features (across all products)
589
+ # ─────────────────────────────────────────────────────────────────────────────
590
+
591
+ shared_features:
592
+ ${config.features.map((f) => ` - ${f}`).join("\n")}
593
+
594
+ # ─────────────────────────────────────────────────────────────────────────────
595
+ # Integration Providers
596
+ # ─────────────────────────────────────────────────────────────────────────────
597
+
598
+ providers:
599
+ ${providersYaml}
600
+
601
+ # ─────────────────────────────────────────────────────────────────────────────
602
+ # Security Configuration
603
+ # ─────────────────────────────────────────────────────────────────────────────
604
+
605
+ security:
606
+ rls_enabled: true
607
+ app_scoped_routes: true
608
+ branded_validators: true
609
+ `;
610
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nyoworks",
3
- "version": "2.7.2",
3
+ "version": "3.1.0",
4
4
  "description": "Create a new NYOWORKS project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,8 @@
30
30
  "execa": "^9.6.1",
31
31
  "fs-extra": "^11.3.3",
32
32
  "picocolors": "^1.1.1",
33
- "prompts": "^2.4.2"
33
+ "prompts": "^2.4.2",
34
+ "yaml": "^2.8.2"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/fs-extra": "^11.0.4",