@veap/plugins 0.0.0-stage → 0.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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/actions/index.d.ts +1 -0
  4. package/dist/actions/index.js +1 -0
  5. package/dist/actions/plugin.actions.d.ts +6 -0
  6. package/dist/actions/plugin.actions.js +60 -0
  7. package/dist/actions.d.ts +1 -0
  8. package/dist/actions.js +1 -0
  9. package/dist/cli/add.d.ts +4 -0
  10. package/dist/cli/add.js +191 -0
  11. package/dist/cli/eject.d.ts +2 -0
  12. package/dist/cli/eject.js +136 -0
  13. package/dist/cli/generate-plugin.d.ts +3 -0
  14. package/dist/cli/generate-plugin.js +48 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.js +6 -0
  17. package/dist/cli/package-manager.d.ts +14 -0
  18. package/dist/cli/package-manager.js +62 -0
  19. package/dist/cli/stubs.d.ts +16 -0
  20. package/dist/cli/stubs.js +42 -0
  21. package/dist/cli/utils.d.ts +3 -0
  22. package/dist/cli/utils.js +164 -0
  23. package/dist/client.d.ts +6 -0
  24. package/dist/client.js +8 -0
  25. package/dist/common/context.d.ts +26 -0
  26. package/dist/common/context.js +26 -0
  27. package/dist/common/events.d.ts +19 -0
  28. package/dist/common/events.js +26 -0
  29. package/dist/common/index.d.ts +3 -0
  30. package/dist/common/index.js +3 -0
  31. package/dist/common/types.d.ts +195 -0
  32. package/dist/common/types.js +40 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +5 -0
  35. package/dist/migrations/0001_create_plugins_tables.d.ts +4 -0
  36. package/dist/migrations/0001_create_plugins_tables.js +29 -0
  37. package/dist/migrations/index.d.ts +4 -0
  38. package/dist/migrations/index.js +7 -0
  39. package/dist/models/SystemPlugin.d.ts +19 -0
  40. package/dist/models/SystemPlugin.js +14 -0
  41. package/dist/models/SystemUserWidget.d.ts +19 -0
  42. package/dist/models/SystemUserWidget.js +17 -0
  43. package/dist/models/index.d.ts +2 -0
  44. package/dist/models/index.js +2 -0
  45. package/dist/models.d.ts +1 -0
  46. package/dist/models.js +1 -0
  47. package/dist/navigation/breadcrumbs.d.ts +2 -0
  48. package/dist/navigation/breadcrumbs.js +84 -0
  49. package/dist/navigation/index.d.ts +2 -0
  50. package/dist/navigation/index.js +2 -0
  51. package/dist/navigation/navigation.service.d.ts +27 -0
  52. package/dist/navigation/navigation.service.js +183 -0
  53. package/dist/providers/index.d.ts +1 -0
  54. package/dist/providers/index.js +1 -0
  55. package/dist/providers/plugins.provider.d.ts +20 -0
  56. package/dist/providers/plugins.provider.js +142 -0
  57. package/dist/registry/contracts.d.ts +72 -0
  58. package/dist/registry/contracts.js +4 -0
  59. package/dist/registry/facade.d.ts +45 -0
  60. package/dist/registry/facade.js +49 -0
  61. package/dist/registry/index.d.ts +5 -0
  62. package/dist/registry/index.js +5 -0
  63. package/dist/registry/migration-runner.d.ts +8 -0
  64. package/dist/registry/migration-runner.js +12 -0
  65. package/dist/registry/plugin.repository.d.ts +19 -0
  66. package/dist/registry/plugin.repository.js +74 -0
  67. package/dist/registry/registry.service.d.ts +69 -0
  68. package/dist/registry/registry.service.js +535 -0
  69. package/dist/router/discovery.d.ts +27 -0
  70. package/dist/router/discovery.js +37 -0
  71. package/dist/router/handler.d.ts +6 -0
  72. package/dist/router/handler.js +40 -0
  73. package/dist/router/index.d.ts +13 -0
  74. package/dist/router/index.js +13 -0
  75. package/dist/router/matcher.d.ts +5 -0
  76. package/dist/router/matcher.js +32 -0
  77. package/dist/router/middlewares.d.ts +25 -0
  78. package/dist/router/middlewares.js +119 -0
  79. package/dist/router/react/error-boundary.d.ts +30 -0
  80. package/dist/router/react/error-boundary.js +68 -0
  81. package/dist/router/react/index.d.ts +4 -0
  82. package/dist/router/react/index.js +4 -0
  83. package/dist/router/react/renderer.d.ts +13 -0
  84. package/dist/router/react/renderer.js +159 -0
  85. package/dist/router/react/soft-navigation.d.ts +4 -0
  86. package/dist/router/react/soft-navigation.js +34 -0
  87. package/dist/router/react/with-router.d.ts +22 -0
  88. package/dist/router/react/with-router.js +19 -0
  89. package/dist/router/route-tree.d.ts +177 -0
  90. package/dist/router/route-tree.js +633 -0
  91. package/dist/router/router.service.d.ts +25 -0
  92. package/dist/router/router.service.js +127 -0
  93. package/dist/router/scanner.d.ts +4 -0
  94. package/dist/router/scanner.js +337 -0
  95. package/dist/router/utils.d.ts +10 -0
  96. package/dist/router/utils.js +48 -0
  97. package/dist/router.d.ts +6 -0
  98. package/dist/router.js +6 -0
  99. package/dist/server.d.ts +11 -0
  100. package/dist/server.js +17 -0
  101. package/dist/widgets/index.d.ts +8 -0
  102. package/dist/widgets/index.js +8 -0
  103. package/dist/widgets/path-prefix-context.d.ts +5 -0
  104. package/dist/widgets/path-prefix-context.js +9 -0
  105. package/dist/widgets/plugin-extension-point-client.d.ts +15 -0
  106. package/dist/widgets/plugin-extension-point-client.js +44 -0
  107. package/dist/widgets/plugin-extension-point.d.ts +14 -0
  108. package/dist/widgets/plugin-extension-point.js +51 -0
  109. package/dist/widgets/plugin-widget-area-client.d.ts +11 -0
  110. package/dist/widgets/plugin-widget-area-client.js +31 -0
  111. package/dist/widgets/plugin-widget-area.d.ts +10 -0
  112. package/dist/widgets/plugin-widget-area.js +19 -0
  113. package/dist/widgets/use-confirm-action.d.ts +19 -0
  114. package/dist/widgets/use-confirm-action.js +68 -0
  115. package/dist/widgets/widget-composer-client.d.ts +21 -0
  116. package/dist/widgets/widget-composer-client.js +281 -0
  117. package/dist/widgets/widget-composer.d.ts +8 -0
  118. package/dist/widgets/widget-composer.js +28 -0
  119. package/package.json +115 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damian Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ [![CI](https://github.com/veapjs/veap-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/veapjs/veap-plugins/actions/workflows/ci.yml)
2
+ [![Release](https://github.com/veapjs/veap-plugins/actions/workflows/release.yml/badge.svg)](https://github.com/veapjs/veap-plugins/actions/workflows/release.yml)
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ # @veap/plugins
5
+
6
+ Plugin architecture, dynamic extension points, virtual routing, and widget
7
+ customization for Veap applications.
8
+
9
+ ## Overview
10
+
11
+ `@veap/plugins` provides an enterprise-grade modular plugin system. It enables
12
+ developers to extend Veap applications dynamically with UI slots, custom
13
+ dashboard widgets, virtual routing, navigation items, and event listeners
14
+ without modifying core application code.
15
+
16
+ ## Features
17
+
18
+ - **Standard plugin lifecycle**: Declarative plugin manifests defining
19
+ dependencies, configuration schemas (Zod), migrations, and activation hooks.
20
+ - **Extension slots & widgets**: React extension points allowing plugins to
21
+ inject custom UI components into existing pages, sidebars, and headers.
22
+ - **Drag-and-drop dashboard widgets**: Built-in support for user-customizable
23
+ widgets with `@dnd-kit` persistence.
24
+ - **Virtual router**: Dynamic route interception and virtual page injection
25
+ via the integrated router service.
26
+ - **Database persistence**: Storing plugin activation states and user widget
27
+ layouts in database tables managed by `@veap/database`.
28
+ - **IoC integration**: `PluginServiceProvider` boots and registers plugin
29
+ capabilities cleanly into the application container.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ # Using bun
35
+ bun add @veap/plugins @veap/kernel @veap/database @veap/auth
36
+
37
+ # Using pnpm
38
+ pnpm add @veap/plugins @veap/kernel @veap/database @veap/auth
39
+
40
+ # Using npm
41
+ npm install @veap/plugins @veap/kernel @veap/database @veap/auth
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Define a Custom Plugin
47
+
48
+ Create a plugin definition implementing the Veap plugin contract:
49
+
50
+ ```tsx
51
+ import type { PluginDefinition } from "@veap/plugins";
52
+
53
+ export const AnalyticsPlugin: PluginDefinition = {
54
+ id: "com.example.analytics",
55
+ name: "Analytics Tracker",
56
+ version: "1.0.0",
57
+ description: "Injects telemetry script and admin dashboard widgets",
58
+
59
+ widgets: [
60
+ {
61
+ id: "analytics-overview",
62
+ title: "Visitor Traffic",
63
+ component: () => (
64
+ <div className="p-4 border rounded">
65
+ <p className="text-sm font-semibold">Active Visitors: 42</p>
66
+ </div>
67
+ ),
68
+ },
69
+ ],
70
+
71
+ async onActivate(container) {
72
+ console.log("Analytics plugin activated!");
73
+ },
74
+ };
75
+ ```
76
+
77
+ ### Register Plugins in Application Builder
78
+
79
+ Pass plugins to `Application.configure()`:
80
+
81
+ ```ts
82
+ import { Application } from "@veap/kernel";
83
+ import { PluginServiceProvider } from "@veap/plugins";
84
+ import { AnalyticsPlugin } from "./plugins/analytics";
85
+
86
+ export const app = Application.configure()
87
+ .use(PluginServiceProvider)
88
+ .withPlugins([AnalyticsPlugin])
89
+ .create();
90
+ ```
91
+
92
+ ### Render Extension Points in React
93
+
94
+ Use extension components from `@veap/plugins/client`:
95
+
96
+ ```tsx
97
+ "use client";
98
+
99
+ import { ExtensionSlot } from "@veap/plugins/client";
100
+
101
+ export function DashboardHeader() {
102
+ return (
103
+ <header className="flex justify-between items-center p-4">
104
+ <h1>Admin Dashboard</h1>
105
+ {/* External plugins can inject actions here */}
106
+ <ExtensionSlot name="dashboard:header:actions" />
107
+ </header>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ## Subpath Exports
113
+
114
+ | Export Path | Content |
115
+ | -------------------------- | -------------------------------------------------------------------------- |
116
+ | `@veap/plugins` | Core plugin definitions, lifecycle management, and `PluginServiceProvider` |
117
+ | `@veap/plugins/client` | React extension slots (`ExtensionSlot`) and widget renderers |
118
+ | `@veap/plugins/router` | Virtual router infrastructure and route rewriting |
119
+ | `@veap/plugins/models` | Active Record models (`PluginModel`, `UserWidgetModel`) |
120
+ | `@veap/plugins/migrations` | Knex migrations for plugin persistence |
121
+
122
+ ## Documentation
123
+
124
+ For full guides and plugin development patterns, visit:
125
+
126
+ - **Official Website**: [https://veap.pl](https://veap.pl)
127
+ - **Documentation**: [https://veap.pl/docs](https://veap.pl/docs)
128
+ - **GitHub Repository**: [https://github.com/veapjs/veap-plugins](https://github.com/veapjs/veap-plugins)
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1 @@
1
+ export { getPluginExtensionsAction, getPluginWidgetsAction, getUserWidgetsState, saveUserWidgetsState, } from "./plugin.actions";
@@ -0,0 +1 @@
1
+ export { getPluginExtensionsAction, getPluginWidgetsAction, getUserWidgetsState, saveUserWidgetsState, } from "./plugin.actions";
@@ -0,0 +1,6 @@
1
+ export declare function getPluginExtensionsAction(target: string, point: string, includeDisabled?: boolean): Promise<import("..").PluginExtension[]>;
2
+ export declare function getPluginWidgetsAction(area: string): Promise<import("..").PluginWidget[]>;
3
+ export declare function getUserWidgetsState(slot: string): Promise<any>;
4
+ export declare function saveUserWidgetsState(slot: string, state: any): Promise<{
5
+ success: boolean;
6
+ }>;
@@ -0,0 +1,60 @@
1
+ "use server";
2
+ import { getCurrentSession } from "@veap/auth";
3
+ import { AppError } from "@veap/kernel";
4
+ import { pluginsContext } from "../common/context";
5
+ import { SystemUserWidget } from "../models/SystemUserWidget";
6
+ export async function getPluginExtensionsAction(target, point, includeDisabled = false) {
7
+ const { user } = await getCurrentSession();
8
+ const userRoles = user?.roles || [];
9
+ const userPermissions = user?.permissions || [];
10
+ return pluginsContext().registry.getExtensions(target, point, includeDisabled, {
11
+ roles: userRoles,
12
+ permissions: userPermissions,
13
+ });
14
+ }
15
+ export async function getPluginWidgetsAction(area) {
16
+ const { user } = await getCurrentSession();
17
+ const userRoles = user?.roles || [];
18
+ const userPermissions = user?.permissions || [];
19
+ return pluginsContext().registry.getWidgets(area, {
20
+ roles: userRoles,
21
+ permissions: userPermissions,
22
+ });
23
+ }
24
+ export async function getUserWidgetsState(slot) {
25
+ const { user } = await getCurrentSession();
26
+ if (!user) {
27
+ return null;
28
+ }
29
+ const widget = await SystemUserWidget.where({
30
+ user_id: user.id,
31
+ slot: slot,
32
+ }).first();
33
+ return widget?.state ?? null;
34
+ }
35
+ export async function saveUserWidgetsState(slot, state) {
36
+ const { user } = await getCurrentSession();
37
+ if (!user) {
38
+ throw AppError.Unauthorized();
39
+ }
40
+ const existing = await SystemUserWidget.where({
41
+ user_id: user.id,
42
+ slot: slot,
43
+ }).first();
44
+ if (existing) {
45
+ await SystemUserWidget.where({
46
+ user_id: user.id,
47
+ slot: slot,
48
+ }).update({
49
+ state: state,
50
+ });
51
+ }
52
+ else {
53
+ await SystemUserWidget.create({
54
+ userId: user.id,
55
+ slot: slot,
56
+ state: state,
57
+ });
58
+ }
59
+ return { success: true };
60
+ }
@@ -0,0 +1 @@
1
+ export * from "./actions/index";
@@ -0,0 +1 @@
1
+ export * from "./actions/index";
@@ -0,0 +1,4 @@
1
+ export declare function addPlugin(pluginArg: string, options?: {
2
+ local?: boolean;
3
+ skipInstall?: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,191 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import ora from "ora";
6
+ import { findProjectRoot, regeneratePluginsRegistry } from "./utils.js";
7
+ async function askQuestion(query) {
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+ return new Promise((resolve) => rl.question(query, (ans) => {
13
+ rl.close();
14
+ resolve(ans);
15
+ }));
16
+ }
17
+ export async function addPlugin(pluginArg, options) {
18
+ const rootDir = findProjectRoot(process.cwd());
19
+ console.log(`\n📦 Adding plugin: ${pluginArg}...`);
20
+ let pluginDir = "";
21
+ let pluginPkgJsonPath = "";
22
+ if (options?.local) {
23
+ // Local installation from Git URL or npm package
24
+ let gitUrl = pluginArg;
25
+ const isGitUrl = /^(git|http|https|github):/.test(pluginArg) || pluginArg.endsWith(".git");
26
+ if (!isGitUrl) {
27
+ const spinUrl = ora(`Fetching Git repository URL for package ${pluginArg}...`).start();
28
+ try {
29
+ const output = execSync(`npm view ${pluginArg} repository.url`, {
30
+ stdio: "pipe",
31
+ })
32
+ .toString()
33
+ .trim();
34
+ if (output) {
35
+ gitUrl = output;
36
+ spinUrl.succeed(`Found repository URL: ${gitUrl}`);
37
+ }
38
+ else {
39
+ spinUrl.fail(`Could not find repository URL for ${pluginArg}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+ catch (_err) {
44
+ spinUrl.fail(`Failed to fetch package details for ${pluginArg}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ // Normalize git URL
49
+ if (gitUrl.startsWith("git+")) {
50
+ gitUrl = gitUrl.slice(4);
51
+ }
52
+ if (gitUrl.startsWith("git://")) {
53
+ gitUrl = `https://${gitUrl.slice(6)}`;
54
+ }
55
+ const repoName = gitUrl
56
+ .split("/")
57
+ .pop()
58
+ ?.replace(/\.git$/, "") || "custom-plugin";
59
+ pluginDir = path.join(rootDir, "plugins", repoName);
60
+ if (fs.existsSync(pluginDir)) {
61
+ console.error(`Error: Local plugin already exists in ${pluginDir}`);
62
+ process.exit(1);
63
+ }
64
+ const spin = ora(`Cloning repository ${gitUrl} to plugins/${repoName}...`).start();
65
+ try {
66
+ execSync(`git clone ${gitUrl} "${pluginDir}"`, { stdio: "ignore" });
67
+ spin.succeed(`Cloned repository to plugins/${repoName}`);
68
+ }
69
+ catch (_err) {
70
+ spin.fail(`Failed to clone repository ${gitUrl}`);
71
+ process.exit(1);
72
+ }
73
+ pluginPkgJsonPath = path.join(pluginDir, "package.json");
74
+ if (fs.existsSync(pluginPkgJsonPath)) {
75
+ try {
76
+ const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgJsonPath, "utf-8"));
77
+ const pluginName = pluginPkg.name || repoName;
78
+ // Add to root package.json as workspace dependency
79
+ const rootPkgPath = path.join(rootDir, "package.json");
80
+ if (fs.existsSync(rootPkgPath)) {
81
+ const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
82
+ rootPkg.dependencies = rootPkg.dependencies || {};
83
+ rootPkg.dependencies[pluginName] = "workspace:*";
84
+ fs.writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`, "utf-8");
85
+ }
86
+ // Run install to link the workspace package
87
+ if (!options?.skipInstall) {
88
+ const { detectPackageManager } = await import("./package-manager.js");
89
+ const pm = detectPackageManager(rootDir);
90
+ const installSpin = ora(`Linking local plugin with ${pm} install...`).start();
91
+ try {
92
+ execSync(`${pm} install`, { cwd: rootDir, stdio: "ignore" });
93
+ installSpin.succeed("Plugin linked successfully.");
94
+ }
95
+ catch (_err) {
96
+ installSpin.fail(`Failed to run ${pm} install automatically.`);
97
+ }
98
+ }
99
+ }
100
+ catch (_) { }
101
+ }
102
+ }
103
+ else {
104
+ // NPM installation via package manager
105
+ if (!options?.skipInstall) {
106
+ const { detectPackageManager } = await import("./package-manager.js");
107
+ const pm = detectPackageManager(rootDir);
108
+ const installCmd = pm === "npm" ? "install" : "add";
109
+ const workspaceFlag = pm === "npm" ? "" : "-w";
110
+ const spin = ora(`Installing npm package ${pluginArg}...`).start();
111
+ try {
112
+ execSync(`${pm} ${installCmd} ${pluginArg} ${workspaceFlag}`.trim(), {
113
+ cwd: rootDir,
114
+ stdio: "inherit",
115
+ });
116
+ spin.succeed(`Package ${pluginArg} installed.`);
117
+ }
118
+ catch (_err) {
119
+ spin.fail(`Failed to install package ${pluginArg}`);
120
+ process.exit(1);
121
+ }
122
+ }
123
+ // Resolve where it is installed in node_modules
124
+ const projectPkgPath = path.join(rootDir, "package.json");
125
+ let resolvedPkgName = pluginArg;
126
+ if (fs.existsSync(projectPkgPath)) {
127
+ const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, "utf-8"));
128
+ const deps = {
129
+ ...(projectPkg.dependencies || {}),
130
+ ...(projectPkg.devDependencies || {}),
131
+ };
132
+ for (const [name, val] of Object.entries(deps)) {
133
+ if (val === pluginArg ||
134
+ (typeof val === "string" && val.includes(pluginArg)) ||
135
+ name === pluginArg) {
136
+ resolvedPkgName = name;
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ pluginDir = path.join(rootDir, "node_modules", resolvedPkgName);
142
+ pluginPkgJsonPath = path.join(pluginDir, "package.json");
143
+ }
144
+ if (!fs.existsSync(pluginPkgJsonPath)) {
145
+ console.error(`Error: Cannot find package.json for plugin at ${pluginPkgJsonPath}`);
146
+ process.exit(1);
147
+ }
148
+ const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgJsonPath, "utf-8"));
149
+ const VeapConfig = pluginPkg.veap || pluginPkg.veap || {};
150
+ // 1. Configure environment variables (.env)
151
+ if (VeapConfig.env) {
152
+ const envPath = path.join(rootDir, ".env");
153
+ let envContent = "";
154
+ if (fs.existsSync(envPath)) {
155
+ envContent = fs.readFileSync(envPath, "utf-8");
156
+ }
157
+ console.log(`\n ⚙️ Configuring environment variables for ${pluginPkg.name || pluginArg}:`);
158
+ // biome-ignore lint/suspicious/noExplicitAny: <ignore>
159
+ for (const [varName, varConfig] of Object.entries(VeapConfig.env)) {
160
+ const regex = new RegExp(`^${varName}=`, "m");
161
+ if (regex.test(envContent)) {
162
+ console.log(` - ${varName} is already configured in .env`);
163
+ continue;
164
+ }
165
+ const desc = varConfig.description || "No description provided";
166
+ const reqStr = varConfig.required ? " (required)" : " (optional)";
167
+ let value = "";
168
+ while (true) {
169
+ value = await askQuestion(`? Enter value for ${varName} - ${desc}${reqStr}: `);
170
+ if (varConfig.required && !value.trim()) {
171
+ console.log(` Error: ${varName} is required.`);
172
+ continue;
173
+ }
174
+ break;
175
+ }
176
+ envContent += `\n# Added by Veap CLI for ${pluginPkg.name}\n${varName}="${value}"\n`;
177
+ }
178
+ fs.writeFileSync(envPath, envContent, "utf-8");
179
+ console.log(`✅ Environment variables saved to .env`);
180
+ }
181
+ // 2. Install dependencies recursively
182
+ if (VeapConfig.dependencies && Array.isArray(VeapConfig.dependencies)) {
183
+ console.log(`\n📦 Installing dependencies for ${pluginPkg.name || pluginArg}...`);
184
+ for (const dep of VeapConfig.dependencies) {
185
+ await addPlugin(dep, options);
186
+ }
187
+ }
188
+ // 3. Regenerate plugins.gen.ts
189
+ regeneratePluginsRegistry(rootDir);
190
+ console.log(`\n✨ Plugin "${pluginPkg.name || pluginArg}" is ready!`);
191
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ejectPackage(packageName: string): Promise<void>;
2
+ export declare const ejectPlugin: typeof ejectPackage;
@@ -0,0 +1,136 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import ora from "ora";
5
+ import { findProjectRoot, regeneratePluginsRegistry } from "./utils.js";
6
+ function normalizeGitUrl(url) {
7
+ if (url.startsWith("git+")) {
8
+ url = url.slice(4);
9
+ }
10
+ if (url.startsWith("git://")) {
11
+ url = `https://${url.slice(6)}`;
12
+ }
13
+ return url;
14
+ }
15
+ export async function ejectPackage(packageName) {
16
+ const rootDir = findProjectRoot(process.cwd());
17
+ // Check if root package.json has it
18
+ const rootPkgPath = path.join(rootDir, "package.json");
19
+ if (!fs.existsSync(rootPkgPath)) {
20
+ console.error("Error: Cannot find root package.json.");
21
+ process.exit(1);
22
+ return;
23
+ }
24
+ const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
25
+ const deps = {
26
+ ...(rootPkg.dependencies || {}),
27
+ ...(rootPkg.devDependencies || {}),
28
+ };
29
+ if (!deps[packageName]) {
30
+ console.error(`Error: Package "${packageName}" is not installed in dependencies.`);
31
+ process.exit(1);
32
+ return;
33
+ return;
34
+ }
35
+ // Find installed folder in node_modules
36
+ const nodeModulesDir = path.join(rootDir, "node_modules", packageName);
37
+ const pkgJsonPath = path.join(nodeModulesDir, "package.json");
38
+ if (!fs.existsSync(pkgJsonPath)) {
39
+ console.error(`Error: Cannot find package.json for installed package at ${pkgJsonPath}`);
40
+ process.exit(1);
41
+ return;
42
+ return;
43
+ }
44
+ const installedPkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
45
+ const isTemplate = installedPkg.veap?.type === "template" ||
46
+ packageName.endsWith("-template") ||
47
+ packageName.includes("template");
48
+ const itemType = isTemplate ? "template" : "plugin";
49
+ const targetSubdir = isTemplate ? "templates" : "plugins";
50
+ console.log(`\n🚀 Ejecting ${itemType}: ${packageName}...`);
51
+ const repo = installedPkg.repository;
52
+ let gitUrl = "";
53
+ if (typeof repo === "string") {
54
+ gitUrl = repo;
55
+ }
56
+ else if (repo && typeof repo === "object") {
57
+ gitUrl = repo.url || "";
58
+ }
59
+ if (!gitUrl) {
60
+ // If not found in package.json, try querying registry
61
+ const spinUrl = ora(`Querying registry for Git repository of ${packageName}...`).start();
62
+ try {
63
+ const output = execSync(`npm view ${packageName} repository.url`, {
64
+ stdio: "pipe",
65
+ })
66
+ .toString()
67
+ .trim();
68
+ if (output) {
69
+ gitUrl = output;
70
+ spinUrl.succeed(`Found repository URL: ${gitUrl}`);
71
+ }
72
+ else {
73
+ spinUrl.fail();
74
+ console.error(`Error: Package package.json does not specify repository URL, and registry query returned nothing.`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ catch (_err) {
79
+ spinUrl.fail();
80
+ console.error(`Error: Could not retrieve repository URL from package or registry.`);
81
+ process.exit(1);
82
+ }
83
+ }
84
+ const normalizedUrl = normalizeGitUrl(gitUrl);
85
+ const folderName = packageName.replace(/^@.*\//, "");
86
+ const destDir = path.join(rootDir, targetSubdir, folderName);
87
+ if (fs.existsSync(destDir)) {
88
+ console.error(`Error: Local ${itemType} directory already exists at ${destDir}`);
89
+ process.exit(1);
90
+ return;
91
+ return;
92
+ }
93
+ // Ensure target parent directory exists (e.g. templates/ or plugins/)
94
+ const targetBaseDir = path.join(rootDir, targetSubdir);
95
+ if (!fs.existsSync(targetBaseDir)) {
96
+ fs.mkdirSync(targetBaseDir, { recursive: true });
97
+ }
98
+ const cloneSpin = ora(`Cloning repository ${normalizedUrl} to ${targetSubdir}/${folderName}...`).start();
99
+ try {
100
+ execSync(`git clone ${normalizedUrl} "${destDir}"`, { stdio: "ignore" });
101
+ cloneSpin.succeed(`Cloned repository to ${targetSubdir}/${folderName}`);
102
+ }
103
+ catch (_err) {
104
+ cloneSpin.fail(`Failed to clone repository ${normalizedUrl}`);
105
+ process.exit(1);
106
+ }
107
+ // Ensure workspace pattern exists in root package.json if workspaces array is defined
108
+ if (Array.isArray(rootPkg.workspaces)) {
109
+ const workspacePattern = `${targetSubdir}/*`;
110
+ if (!rootPkg.workspaces.includes(workspacePattern)) {
111
+ rootPkg.workspaces.push(workspacePattern);
112
+ }
113
+ }
114
+ // Update root package.json dependency to workspace:*
115
+ rootPkg.dependencies = rootPkg.dependencies || {};
116
+ rootPkg.dependencies[packageName] = "workspace:*";
117
+ fs.writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`, "utf-8");
118
+ console.log(`✅ Updated root package.json dependencies.`);
119
+ // Link workspace packages
120
+ const { detectPackageManager } = await import("./package-manager.js");
121
+ const pm = detectPackageManager(rootDir);
122
+ const linkSpin = ora(`Running ${pm} install to link ejected ${itemType}...`).start();
123
+ try {
124
+ execSync(`${pm} install`, { cwd: rootDir, stdio: "ignore" });
125
+ linkSpin.succeed(`${isTemplate ? "Template" : "Plugin"} linked successfully.`);
126
+ }
127
+ catch (_err) {
128
+ linkSpin.fail(`Failed to run ${pm} install automatically.`);
129
+ }
130
+ // Regenerate registry only for plugins
131
+ if (!isTemplate) {
132
+ regeneratePluginsRegistry(rootDir);
133
+ }
134
+ console.log(`\n✨ ${isTemplate ? "Template" : "Plugin"} "${packageName}" ejected successfully to local folder ${targetSubdir}/${folderName}!`);
135
+ }
136
+ export const ejectPlugin = ejectPackage;
@@ -0,0 +1,3 @@
1
+ export declare function generatePlugin(name: string, options?: {
2
+ skipInstall?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,48 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import ora from "ora";
5
+ import { stubPath } from "./stubs.js";
6
+ import { findProjectRoot, processStubs, } from "./utils.js";
7
+ export async function generatePlugin(name, options) {
8
+ const rootDir = findProjectRoot(process.cwd());
9
+ const pluginDir = path.join(rootDir, "plugins", `${name}-plugin`);
10
+ if (fs.existsSync(pluginDir)) {
11
+ console.error(`Error: Plugin "${name}" already exists in ${pluginDir}`);
12
+ process.exit(1);
13
+ }
14
+ const rootPluginsDir = path.join(rootDir, "plugins");
15
+ if (!fs.existsSync(rootPluginsDir)) {
16
+ fs.mkdirSync(rootPluginsDir, { recursive: true });
17
+ }
18
+ console.log(`🚀 Generating a plugin: ${name} in ${pluginDir}...`);
19
+ try {
20
+ const titleName = name
21
+ .split("-")
22
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
23
+ .join(" ");
24
+ const pascalName = name.replace(/-/g, "");
25
+ const date = new Date().toDateString();
26
+ const variables = {
27
+ name,
28
+ titleName,
29
+ pascalName,
30
+ date,
31
+ };
32
+ // Stubs live at the package root of the installed @veap/framework.
33
+ const stubsDir = stubPath("plugin");
34
+ const spin = ora("Generating plugin files from stubs...").start();
35
+ processStubs(stubsDir, pluginDir, variables);
36
+ spin.succeed("Plugin files generated.");
37
+ if (!options?.skipInstall) {
38
+ const spinner = ora("📦 Installing dependencies (pnpm install)...").start();
39
+ execSync("pnpm install", { cwd: rootDir, stdio: "inherit" });
40
+ spinner.succeed("Dependencies installed.");
41
+ }
42
+ console.log(`\n✨ Plugin "${name}" is ready!`);
43
+ console.log(`💡 Run "pnpm veap generate:plugins" to register it.`);
44
+ }
45
+ catch (err) {
46
+ console.error("Error generating plugin:", err);
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ export { addPlugin } from "./add";
2
+ export { ejectPackage, ejectPlugin } from "./eject";
3
+ export { generatePlugin } from "./generate-plugin";
4
+ export { applyDockerfileForPackageManager, detectPackageManager, type PackageManager, } from "./package-manager";
5
+ export { findStubsDir, stubPath } from "./stubs";
6
+ export { findProjectRoot, processStubs, regeneratePluginsRegistry, } from "./utils";
@@ -0,0 +1,6 @@
1
+ export { addPlugin } from "./add";
2
+ export { ejectPackage, ejectPlugin } from "./eject";
3
+ export { generatePlugin } from "./generate-plugin";
4
+ export { applyDockerfileForPackageManager, detectPackageManager, } from "./package-manager";
5
+ export { findStubsDir, stubPath } from "./stubs";
6
+ export { findProjectRoot, processStubs, regeneratePluginsRegistry, } from "./utils";
@@ -0,0 +1,14 @@
1
+ export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
2
+ /**
3
+ * Detects the package manager used by the project in `dir`.
4
+ *
5
+ * Priority: `packageManager` field in package.json > lockfiles > default (pnpm).
6
+ * On conflicting lockfiles pnpm wins (framework default).
7
+ */
8
+ export declare function detectPackageManager(dir: string): PackageManager;
9
+ /**
10
+ * Overwrites the generated pnpm Dockerfile with the variant matching `pm`.
11
+ * No-op for pnpm (baseline stub), missing variants or missing Dockerfile -
12
+ * best effort, Docker config is optional.
13
+ */
14
+ export declare function applyDockerfileForPackageManager(projectDir: string, pm: PackageManager): void;