@jskit-ai/feature-server-generator 0.1.1

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,268 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@jskit-ai/feature-server-generator",
4
+ version: "0.1.1",
5
+ kind: "generator",
6
+ description: "Scaffold substantial non-CRUD server feature packages with provider, actions, service, and optional persistence seams.",
7
+ options: {
8
+ "feature-name": {
9
+ required: true,
10
+ inputType: "text",
11
+ defaultValue: "",
12
+ promptLabel: "Feature name",
13
+ promptHint: "Required feature slug (example: booking-engine, availability-engine, billing-engine)."
14
+ },
15
+ mode: {
16
+ required: false,
17
+ inputType: "text",
18
+ defaultValue: "json-rest",
19
+ validationType: "enum",
20
+ allowedValues: ["json-rest", "orchestrator", "custom-knex"],
21
+ promptLabel: "Scaffold mode",
22
+ promptHint: "json-rest | orchestrator | custom-knex"
23
+ },
24
+ surface: {
25
+ required: false,
26
+ inputType: "text",
27
+ validationType: "enabled-surface-id",
28
+ promptLabel: "Target surface",
29
+ promptHint: "Optional surface id for generated action and route metadata."
30
+ },
31
+ "route-prefix": {
32
+ required: false,
33
+ inputType: "text",
34
+ defaultValue: "",
35
+ promptLabel: "Route prefix",
36
+ promptHint: "Optional relative API path (example: admin/booking-engine)."
37
+ },
38
+ force: {
39
+ required: false,
40
+ inputType: "flag",
41
+ defaultValue: "",
42
+ promptLabel: "Force overwrite",
43
+ promptHint: "Overwrite generated scaffold files if the feature package directory already exists."
44
+ }
45
+ },
46
+ dependsOn: [],
47
+ capabilities: {
48
+ provides: ["feature-server-generator"],
49
+ requires: []
50
+ },
51
+ runtime: {
52
+ server: {
53
+ providers: []
54
+ },
55
+ client: {
56
+ providers: []
57
+ }
58
+ },
59
+ metadata: {
60
+ generatorPrimarySubcommand: "scaffold",
61
+ generatorSubcommands: {
62
+ scaffold: {
63
+ description: "Create a dedicated non-CRUD server feature package with provider, actions, service, and optional repository/routes.",
64
+ longDescription: [
65
+ "This is the standard JSKIT starting point for substantial non-CRUD server features such as engines, workflows, and policy services.",
66
+ "The default mode is json-rest. Use orchestrator for non-persistent coordination packages, or custom-knex only when raw knex is an explicit exception."
67
+ ],
68
+ positionalArgs: [
69
+ {
70
+ name: "feature-name",
71
+ required: true,
72
+ description: "Feature slug used for the package path, provider name, inline action ids, and default container tokens."
73
+ }
74
+ ],
75
+ optionNames: ["feature-name", "mode", "surface", "route-prefix", "force"],
76
+ createTarget: {
77
+ pathTemplate: "packages/${option:feature-name|kebab}",
78
+ label: "package directory",
79
+ allowExistingEmptyDirectory: false
80
+ },
81
+ notes: [
82
+ "json-rest emits a repository that starts from internal json-rest-api seams.",
83
+ "orchestrator omits repository.js entirely and keeps the service focused on coordination.",
84
+ "custom-knex is the explicit weird/custom lane and emits a repository wired to jskit.database.knex.",
85
+ "If --route-prefix is omitted, registerRoutes.js is not generated."
86
+ ],
87
+ examples: [
88
+ {
89
+ label: "Default persistent feature",
90
+ lines: [
91
+ "npx jskit generate feature-server-generator scaffold \\",
92
+ " booking-engine"
93
+ ]
94
+ },
95
+ {
96
+ label: "Non-persistent orchestrator",
97
+ lines: [
98
+ "npx jskit generate feature-server-generator scaffold \\",
99
+ " availability-engine \\",
100
+ " --mode orchestrator"
101
+ ]
102
+ },
103
+ {
104
+ label: "Another default persistent feature",
105
+ lines: [
106
+ "npx jskit generate feature-server-generator scaffold \\",
107
+ " billing-engine"
108
+ ]
109
+ },
110
+ {
111
+ label: "Rare explicit custom-knex feature",
112
+ lines: [
113
+ "npx jskit generate feature-server-generator scaffold \\",
114
+ " invoice-rollup \\",
115
+ " --mode custom-knex \\",
116
+ " --route-prefix admin/invoice-rollup \\",
117
+ " --surface admin"
118
+ ]
119
+ }
120
+ ]
121
+ }
122
+ },
123
+ apiSummary: {
124
+ surfaces: [
125
+ {
126
+ subpath: "./server/buildTemplateContext",
127
+ summary: "Builds deterministic non-CRUD feature server scaffold template context values."
128
+ }
129
+ ],
130
+ containerTokens: {
131
+ server: [],
132
+ client: []
133
+ }
134
+ },
135
+ jskit: {
136
+ ownershipGuidance: {
137
+ title: "Standard non-CRUD server lane",
138
+ summary: "Use this generator when a substantial server feature should become its own package instead of growing inside packages/main.",
139
+ responsibilities: [
140
+ "provider: wires DI, actions, repository, and optional routes",
141
+ "service: owns orchestration and must not talk to persistence directly",
142
+ "repository: owns persistence; default-lane persistent scaffolds start from internal json-rest-api",
143
+ "packages/main: stays composition/glue only"
144
+ ],
145
+ examples: [
146
+ "jskit generate feature-server-generator scaffold booking-engine",
147
+ "jskit generate feature-server-generator scaffold availability-engine --mode orchestrator",
148
+ "jskit generate feature-server-generator scaffold billing-engine"
149
+ ]
150
+ }
151
+ }
152
+ },
153
+ mutations: {
154
+ dependencies: {
155
+ runtime: {
156
+ "@jskit-ai/database-runtime": "0.1.59",
157
+ "@jskit-ai/database-runtime-mysql": "0.1.58",
158
+ "@jskit-ai/json-rest-api-core": "0.1.4",
159
+ "@jskit-ai/kernel": "0.1.59",
160
+ "json-rest-schema": "1.x.x",
161
+ "@local/${option:feature-name|kebab}": "file:packages/${option:feature-name|kebab}"
162
+ },
163
+ dev: {}
164
+ },
165
+ packageJson: {
166
+ scripts: {}
167
+ },
168
+ procfile: {},
169
+ files: [
170
+ {
171
+ from: "templates/src/local-package/package.json",
172
+ to: "packages/${option:feature-name|kebab}/package.json",
173
+ reason: "Install app-local non-CRUD feature package manifest.",
174
+ category: "feature-server-generator",
175
+ id: "feature-server-package-json-${option:feature-name|snake}"
176
+ },
177
+ {
178
+ from: "templates/src/local-package/package.descriptor.mjs",
179
+ to: "packages/${option:feature-name|kebab}/package.descriptor.mjs",
180
+ reason: "Install app-local non-CRUD feature package descriptor.",
181
+ category: "feature-server-generator",
182
+ id: "feature-server-package-descriptor-${option:feature-name|snake}",
183
+ templateContext: {
184
+ entrypoint: "src/server/buildTemplateContext.js",
185
+ export: "buildTemplateContext"
186
+ }
187
+ },
188
+ {
189
+ from: "templates/src/local-package/server/FeatureProvider.js",
190
+ to: "packages/${option:feature-name|kebab}/src/server/${option:feature-name|pascal}Provider.js",
191
+ reason: "Install app-local non-CRUD feature provider.",
192
+ category: "feature-server-generator",
193
+ id: "feature-server-provider-${option:feature-name|snake}",
194
+ templateContext: {
195
+ entrypoint: "src/server/buildTemplateContext.js",
196
+ export: "buildTemplateContext"
197
+ }
198
+ },
199
+ {
200
+ from: "templates/src/local-package/server/inputSchemas.js",
201
+ to: "packages/${option:feature-name|kebab}/src/server/inputSchemas.js",
202
+ reason: "Install generated feature action and route validators.",
203
+ category: "feature-server-generator",
204
+ id: "feature-server-input-schemas-${option:feature-name|snake}"
205
+ },
206
+ {
207
+ from: "templates/src/local-package/server/actions.js",
208
+ to: "packages/${option:feature-name|kebab}/src/server/actions.js",
209
+ reason: "Install generated feature action definitions.",
210
+ category: "feature-server-generator",
211
+ id: "feature-server-actions-${option:feature-name|snake}",
212
+ templateContext: {
213
+ entrypoint: "src/server/buildTemplateContext.js",
214
+ export: "buildTemplateContext"
215
+ }
216
+ },
217
+ {
218
+ from: "templates/src/local-package/server/service.js",
219
+ to: "packages/${option:feature-name|kebab}/src/server/service.js",
220
+ reason: "Install generated feature service.",
221
+ category: "feature-server-generator",
222
+ id: "feature-server-service-${option:feature-name|snake}",
223
+ templateContext: {
224
+ entrypoint: "src/server/buildTemplateContext.js",
225
+ export: "buildTemplateContext"
226
+ }
227
+ },
228
+ {
229
+ from: "templates/src/local-package/server/registerRoutes.js",
230
+ to: "packages/${option:feature-name|kebab}/src/server/registerRoutes.js",
231
+ reason: "Install generated feature route registration.",
232
+ category: "feature-server-generator",
233
+ id: "feature-server-routes-${option:feature-name|snake}",
234
+ templateContext: {
235
+ entrypoint: "src/server/buildTemplateContext.js",
236
+ export: "buildTemplateContext"
237
+ },
238
+ when: {
239
+ option: "route-prefix",
240
+ hasText: true
241
+ }
242
+ },
243
+ {
244
+ from: "templates/src/local-package/server/repositoryJsonRest.js",
245
+ to: "packages/${option:feature-name|kebab}/src/server/repository.js",
246
+ reason: "Install generated feature repository with internal json-rest-api seam.",
247
+ category: "feature-server-generator",
248
+ id: "feature-server-repository-json-rest-${option:feature-name|snake}",
249
+ when: {
250
+ option: "mode",
251
+ equals: "json-rest"
252
+ }
253
+ },
254
+ {
255
+ from: "templates/src/local-package/server/repositoryCustomKnex.js",
256
+ to: "packages/${option:feature-name|kebab}/src/server/repository.js",
257
+ reason: "Install generated feature repository with explicit knex seam.",
258
+ category: "feature-server-generator",
259
+ id: "feature-server-repository-custom-knex-${option:feature-name|snake}",
260
+ when: {
261
+ option: "mode",
262
+ equals: "custom-knex"
263
+ }
264
+ }
265
+ ],
266
+ text: []
267
+ }
268
+ });
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@jskit-ai/feature-server-generator",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ }
8
+ }
@@ -0,0 +1,274 @@
1
+ function splitTextIntoWords(value) {
2
+ const normalized = String(value || "")
3
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
4
+ .replace(/[^A-Za-z0-9]+/g, " ")
5
+ .trim();
6
+ if (!normalized) {
7
+ return [];
8
+ }
9
+
10
+ return normalized
11
+ .split(/\s+/u)
12
+ .map((entry) => String(entry || "").trim().toLowerCase())
13
+ .filter(Boolean);
14
+ }
15
+
16
+ function wordsToPascal(words = []) {
17
+ return words
18
+ .map((entry) => {
19
+ const value = String(entry || "").trim().toLowerCase();
20
+ if (!value) {
21
+ return "";
22
+ }
23
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
24
+ })
25
+ .join("");
26
+ }
27
+
28
+ function wordsToKebab(words = []) {
29
+ return words
30
+ .map((entry) => String(entry || "").trim().toLowerCase())
31
+ .filter(Boolean)
32
+ .join("-");
33
+ }
34
+
35
+ function wordsToCamel(words = []) {
36
+ const pascal = wordsToPascal(words);
37
+ if (!pascal) {
38
+ return "";
39
+ }
40
+ return `${pascal.slice(0, 1).toLowerCase()}${pascal.slice(1)}`;
41
+ }
42
+
43
+ function normalizeFeatureName(value) {
44
+ const normalized = wordsToKebab(splitTextIntoWords(value));
45
+ if (!normalized) {
46
+ throw new Error("feature-server-generator requires option feature-name.");
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ function normalizeSurfaceId(value) {
52
+ return String(value || "").trim().toLowerCase();
53
+ }
54
+
55
+ function normalizeRoutePrefix(value) {
56
+ return String(value || "")
57
+ .split("/")
58
+ .map((segment) => {
59
+ const normalizedSegment = String(segment || "").trim();
60
+ if (!normalizedSegment) {
61
+ return "";
62
+ }
63
+ if (normalizedSegment.startsWith(":")) {
64
+ return normalizedSegment;
65
+ }
66
+ return wordsToKebab(splitTextIntoWords(normalizedSegment));
67
+ })
68
+ .filter(Boolean)
69
+ .join("/");
70
+ }
71
+
72
+ function quoteArray(values = []) {
73
+ return values.map((entry) => JSON.stringify(String(entry || ""))).join(", ");
74
+ }
75
+
76
+ function buildProviderContext({ featureName, mode, routePrefix, surface }) {
77
+ const featureToken = `feature.${featureName}`;
78
+ const isJsonRest = mode === "json-rest";
79
+ const isCustomKnex = mode === "custom-knex";
80
+ const hasRoutes = Boolean(routePrefix);
81
+
82
+ const dependsOn = ["runtime.actions"];
83
+ if (isJsonRest) {
84
+ dependsOn.push("json-rest-api.core");
85
+ }
86
+ if (isCustomKnex) {
87
+ dependsOn.push("runtime.database");
88
+ }
89
+
90
+ let repositoryImport = "";
91
+ let repositoryRegistration = "";
92
+ let serviceFactoryArg = "{}";
93
+ if (isJsonRest) {
94
+ repositoryImport = [
95
+ 'import { INTERNAL_JSON_REST_API } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";',
96
+ 'import { createRepository } from "./repository.js";'
97
+ ].join("\n");
98
+ repositoryRegistration = [
99
+ ` app.singleton("${featureToken}.repository", (scope) => {`,
100
+ " return createRepository({",
101
+ " api: scope.make(INTERNAL_JSON_REST_API)",
102
+ " });",
103
+ " });",
104
+ ""
105
+ ].join("\n");
106
+ serviceFactoryArg = `{ featureRepository: _scope.make("${featureToken}.repository") }`;
107
+ } else if (isCustomKnex) {
108
+ repositoryImport = 'import { createRepository } from "./repository.js";';
109
+ repositoryRegistration = [
110
+ ` app.singleton("${featureToken}.repository", (scope) => {`,
111
+ " return createRepository({",
112
+ ' knex: scope.make("jskit.database.knex")',
113
+ " });",
114
+ " });",
115
+ ""
116
+ ].join("\n");
117
+ serviceFactoryArg = `{ featureRepository: _scope.make("${featureToken}.repository") }`;
118
+ }
119
+
120
+ let routeImport = "";
121
+ let bootMethod = " boot() {}";
122
+ if (hasRoutes) {
123
+ const routeLines = [
124
+ " boot(app) {",
125
+ " registerRoutes(app, {",
126
+ ` routeRelativePath: ${JSON.stringify(routePrefix)},`
127
+ ];
128
+ if (surface) {
129
+ routeLines.push(` routeSurface: ${JSON.stringify(surface)}`);
130
+ }
131
+ routeLines.push(" });");
132
+ routeLines.push(" }");
133
+ routeImport = 'import { registerRoutes } from "./registerRoutes.js";';
134
+ bootMethod = routeLines.join("\n");
135
+ }
136
+
137
+ return Object.freeze({
138
+ "__JSKIT_FEATURE_PROVIDER_DEPENDS_ON__": quoteArray(dependsOn),
139
+ "__JSKIT_FEATURE_PROVIDER_REPOSITORY_IMPORT__": repositoryImport,
140
+ "__JSKIT_FEATURE_PROVIDER_ROUTE_IMPORT__": routeImport,
141
+ "__JSKIT_FEATURE_PROVIDER_REPOSITORY_REGISTRATION__": repositoryRegistration,
142
+ "__JSKIT_FEATURE_PROVIDER_SERVICE_FACTORY_ARG__": serviceFactoryArg,
143
+ "__JSKIT_FEATURE_PROVIDER_BOOT_METHOD__": bootMethod
144
+ });
145
+ }
146
+
147
+ function buildActionsContext({ surface }) {
148
+ const surfacesLine = surface
149
+ ? ` surfaces: [${JSON.stringify(surface)}],`
150
+ : ' surfacesFrom: "enabled",';
151
+
152
+ return Object.freeze({
153
+ "__JSKIT_FEATURE_ACTION_SURFACES_LINE__": surfacesLine
154
+ });
155
+ }
156
+
157
+ function buildServiceContext({ featureName, mode }) {
158
+ const featureToken = `feature.${featureName}`;
159
+ const isPersistent = mode === "json-rest" || mode === "custom-knex";
160
+ if (!isPersistent) {
161
+ return Object.freeze({
162
+ "__JSKIT_FEATURE_SERVICE_REPOSITORY_GUARD__": " void featureRepository;",
163
+ "__JSKIT_FEATURE_SERVICE_GET_STATUS_BODY__": [
164
+ " void options;",
165
+ " return {",
166
+ ` ok: true,`,
167
+ ` feature: ${JSON.stringify(featureName)},`,
168
+ ` mode: ${JSON.stringify(mode)},`,
169
+ " customized: false,",
170
+ " input,",
171
+ ' message: "Replace createService().getStatus() with feature-specific orchestration logic."',
172
+ " };"
173
+ ].join("\n"),
174
+ "__JSKIT_FEATURE_SERVICE_EXECUTE_BODY__": [
175
+ " void options;",
176
+ " return {",
177
+ " accepted: false,",
178
+ ` feature: ${JSON.stringify(featureName)},`,
179
+ ` mode: ${JSON.stringify(mode)},`,
180
+ " customized: false,",
181
+ " input,",
182
+ ' message: "Replace createService().execute() with feature-specific orchestration logic."',
183
+ " };"
184
+ ].join("\n")
185
+ });
186
+ }
187
+
188
+ return Object.freeze({
189
+ "__JSKIT_FEATURE_SERVICE_REPOSITORY_GUARD__": [
190
+ " if (!featureRepository) {",
191
+ ` throw new TypeError("createService requires ${featureToken}.repository.");`,
192
+ " }"
193
+ ].join("\n"),
194
+ "__JSKIT_FEATURE_SERVICE_GET_STATUS_BODY__": [
195
+ " return featureRepository.getStatus(input, {",
196
+ " context: options?.context || null",
197
+ " });"
198
+ ].join("\n"),
199
+ "__JSKIT_FEATURE_SERVICE_EXECUTE_BODY__": [
200
+ " return featureRepository.execute(input, {",
201
+ " context: options?.context || null,",
202
+ " trx: options?.trx || null",
203
+ " });"
204
+ ].join("\n")
205
+ });
206
+ }
207
+
208
+ function buildRouteContext({ surface }) {
209
+ const routeSurfaceImport = surface ? ", normalizeSurfaceId" : "";
210
+ const routeSurfaceNormalizerLine = surface
211
+ ? " const normalizedRouteSurface = normalizeSurfaceId(routeSurface);"
212
+ : " void routeSurface;";
213
+ const routeSurfaceLine = surface ? " surface: normalizedRouteSurface," : "";
214
+
215
+ return Object.freeze({
216
+ "__JSKIT_FEATURE_ROUTE_SURFACE_IMPORT__": routeSurfaceImport,
217
+ "__JSKIT_FEATURE_ROUTE_SURFACE_NORMALIZER_LINE__": routeSurfaceNormalizerLine,
218
+ "__JSKIT_FEATURE_ROUTE_SURFACE_LINE__": routeSurfaceLine
219
+ });
220
+ }
221
+
222
+ function buildDescriptorContext({ featureName, mode }) {
223
+ const isJsonRest = mode === "json-rest";
224
+ const isCustomKnex = mode === "custom-knex";
225
+ const isPersistent = isJsonRest || isCustomKnex;
226
+ const dependsOnLines = [];
227
+ if (isJsonRest) {
228
+ dependsOnLines.push(' "@jskit-ai/json-rest-api-core"');
229
+ }
230
+ if (isCustomKnex) {
231
+ dependsOnLines.push(' "@jskit-ai/database-runtime"');
232
+ }
233
+
234
+ const descriptorDependsOnLines = dependsOnLines.length > 0
235
+ ? `,\n${dependsOnLines.join(",\n")}`
236
+ : "";
237
+ const descriptorRepositoryTokenLine = isPersistent ? `,\n "feature.${featureName}.repository"` : "";
238
+ const lane = isCustomKnex ? "weird-custom" : "default";
239
+
240
+ return Object.freeze({
241
+ "__JSKIT_FEATURE_DESCRIPTOR_DEPENDS_ON_LINES__": descriptorDependsOnLines,
242
+ "__JSKIT_FEATURE_DESCRIPTOR_CAPABILITY_REQUIRES_LINES__": "",
243
+ "__JSKIT_FEATURE_DESCRIPTOR_REPOSITORY_TOKEN_LINE__": descriptorRepositoryTokenLine,
244
+ "__JSKIT_FEATURE_DESCRIPTOR_LANE__": lane
245
+ });
246
+ }
247
+
248
+ async function buildTemplateContext({ options = {} } = {}) {
249
+ const featureName = normalizeFeatureName(options["feature-name"]);
250
+ const mode = String(options.mode || "json-rest").trim() || "json-rest";
251
+ const routePrefix = normalizeRoutePrefix(options["route-prefix"]);
252
+ const surface = normalizeSurfaceId(options.surface);
253
+ const context = {
254
+ "__JSKIT_FEATURE_NAME_KEBAB__": featureName,
255
+ "__JSKIT_FEATURE_NAME_PASCAL__": wordsToPascal(splitTextIntoWords(featureName)),
256
+ "__JSKIT_FEATURE_NAME_CAMEL__": wordsToCamel(splitTextIntoWords(featureName))
257
+ };
258
+
259
+ return Object.freeze({
260
+ ...context,
261
+ ...buildProviderContext({
262
+ featureName,
263
+ mode,
264
+ routePrefix,
265
+ surface
266
+ }),
267
+ ...buildActionsContext({ surface }),
268
+ ...buildServiceContext({ featureName, mode }),
269
+ ...buildRouteContext({ surface }),
270
+ ...buildDescriptorContext({ featureName, mode })
271
+ });
272
+ }
273
+
274
+ export { buildTemplateContext };
@@ -0,0 +1,64 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@local/${option:feature-name|kebab}",
4
+ version: "0.1.0",
5
+ kind: "runtime",
6
+ description: "App-local non-CRUD feature package (${option:feature-name|kebab}).",
7
+ dependsOn: [
8
+ "@jskit-ai/kernel"__JSKIT_FEATURE_DESCRIPTOR_DEPENDS_ON_LINES__
9
+ ],
10
+ capabilities: {
11
+ provides: [
12
+ "feature.${option:feature-name|kebab}"
13
+ ],
14
+ requires: [
15
+ "runtime.actions"__JSKIT_FEATURE_DESCRIPTOR_CAPABILITY_REQUIRES_LINES__
16
+ ]
17
+ },
18
+ runtime: {
19
+ server: {
20
+ providers: [
21
+ {
22
+ entrypoint: "src/server/${option:feature-name|pascal}Provider.js",
23
+ export: "${option:feature-name|pascal}Provider"
24
+ }
25
+ ]
26
+ },
27
+ client: {
28
+ providers: []
29
+ }
30
+ },
31
+ metadata: {
32
+ apiSummary: {
33
+ surfaces: [
34
+ {
35
+ subpath: "./server/actions",
36
+ summary: "Exports generated feature action definitions with inline starter ids."
37
+ }
38
+ ],
39
+ containerTokens: {
40
+ server: [
41
+ "feature.${option:feature-name|kebab}.service"__JSKIT_FEATURE_DESCRIPTOR_REPOSITORY_TOKEN_LINE__
42
+ ],
43
+ client: []
44
+ }
45
+ },
46
+ jskit: {
47
+ scaffoldShape: "feature-server-v1",
48
+ scaffoldMode: "${option:mode}",
49
+ lane: "__JSKIT_FEATURE_DESCRIPTOR_LANE__"
50
+ }
51
+ },
52
+ mutations: {
53
+ dependencies: {
54
+ runtime: {},
55
+ dev: {}
56
+ },
57
+ packageJson: {
58
+ scripts: {}
59
+ },
60
+ procfile: {},
61
+ files: [],
62
+ text: []
63
+ }
64
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@local/${option:feature-name|kebab}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ "./server/actions": "./src/server/actions.js"
8
+ }
9
+ }
@@ -0,0 +1,43 @@
1
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
2
+ __JSKIT_FEATURE_PROVIDER_REPOSITORY_IMPORT__
3
+ import { createService } from "./service.js";
4
+ import { featureActions } from "./actions.js";
5
+ __JSKIT_FEATURE_PROVIDER_ROUTE_IMPORT__
6
+
7
+ class ${option:feature-name|pascal}Provider {
8
+ static id = "feature.${option:feature-name|kebab}";
9
+
10
+ static dependsOn = [__JSKIT_FEATURE_PROVIDER_DEPENDS_ON__];
11
+
12
+ register(app) {
13
+ if (
14
+ !app ||
15
+ typeof app.singleton !== "function" ||
16
+ typeof app.service !== "function" ||
17
+ typeof app.actions !== "function"
18
+ ) {
19
+ throw new Error("${option:feature-name|pascal}Provider requires application singleton()/service()/actions().");
20
+ }
21
+
22
+ __JSKIT_FEATURE_PROVIDER_REPOSITORY_REGISTRATION__
23
+ app.service(
24
+ "feature.${option:feature-name|kebab}.service",
25
+ (_scope) => {
26
+ return createService(__JSKIT_FEATURE_PROVIDER_SERVICE_FACTORY_ARG__);
27
+ }
28
+ );
29
+
30
+ app.actions(
31
+ withActionDefaults(featureActions, {
32
+ domain: "feature",
33
+ dependencies: {
34
+ featureService: "feature.${option:feature-name|kebab}.service"
35
+ }
36
+ })
37
+ );
38
+ }
39
+
40
+ __JSKIT_FEATURE_PROVIDER_BOOT_METHOD__
41
+ }
42
+
43
+ export { ${option:feature-name|pascal}Provider };
@@ -0,0 +1,50 @@
1
+ import {
2
+ statusQueryInputValidator,
3
+ executeCommandInputValidator
4
+ } from "./inputSchemas.js";
5
+
6
+ const ACTION_GET_STATUS = "feature.${option:feature-name|kebab}.status.read";
7
+ const ACTION_EXECUTE = "feature.${option:feature-name|kebab}.execute";
8
+
9
+ const featureActions = Object.freeze([
10
+ {
11
+ id: ACTION_GET_STATUS,
12
+ version: 1,
13
+ kind: "query",
14
+ channels: ["api", "automation", "internal"],
15
+ __JSKIT_FEATURE_ACTION_SURFACES_LINE__
16
+ input: statusQueryInputValidator,
17
+ output: null,
18
+ idempotency: "none",
19
+ audit: {
20
+ actionName: ACTION_GET_STATUS
21
+ },
22
+ observability: {},
23
+ async execute(input, context, deps) {
24
+ return deps.featureService.getStatus(input, {
25
+ context
26
+ });
27
+ }
28
+ },
29
+ {
30
+ id: ACTION_EXECUTE,
31
+ version: 1,
32
+ kind: "command",
33
+ channels: ["api", "automation", "internal"],
34
+ __JSKIT_FEATURE_ACTION_SURFACES_LINE__
35
+ input: executeCommandInputValidator,
36
+ output: null,
37
+ idempotency: "optional",
38
+ audit: {
39
+ actionName: ACTION_EXECUTE
40
+ },
41
+ observability: {},
42
+ async execute(input, context, deps) {
43
+ return deps.featureService.execute(input, {
44
+ context
45
+ });
46
+ }
47
+ }
48
+ ]);
49
+
50
+ export { featureActions };
@@ -0,0 +1,38 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
+
4
+ const statusQueryInputValidator = deepFreeze({
5
+ schema: createSchema({
6
+ scope: {
7
+ type: "string",
8
+ required: false,
9
+ minLength: 1
10
+ },
11
+ verbose: {
12
+ type: "boolean",
13
+ required: false
14
+ }
15
+ }),
16
+ mode: "patch"
17
+ });
18
+
19
+ const executeCommandInputValidator = deepFreeze({
20
+ schema: createSchema({
21
+ command: {
22
+ type: "string",
23
+ required: true,
24
+ minLength: 1
25
+ },
26
+ payload: {
27
+ type: "object",
28
+ required: false,
29
+ additionalProperties: true
30
+ }
31
+ }),
32
+ mode: "patch"
33
+ });
34
+
35
+ export {
36
+ statusQueryInputValidator,
37
+ executeCommandInputValidator
38
+ };
@@ -0,0 +1,74 @@
1
+ import { resolveScopedApiBasePath__JSKIT_FEATURE_ROUTE_SURFACE_IMPORT__ } from "@jskit-ai/kernel/shared/surface";
2
+ import {
3
+ statusQueryInputValidator,
4
+ executeCommandInputValidator
5
+ } from "./inputSchemas.js";
6
+
7
+ const ACTION_GET_STATUS = "feature.${option:feature-name|kebab}.status.read";
8
+ const ACTION_EXECUTE = "feature.${option:feature-name|kebab}.execute";
9
+
10
+ function registerRoutes(
11
+ app,
12
+ {
13
+ routeSurface = "",
14
+ routeRelativePath = ""
15
+ } = {}
16
+ ) {
17
+ if (!app || typeof app.make !== "function") {
18
+ throw new Error("registerRoutes requires application make().");
19
+ }
20
+
21
+ const router = app.make("jskit.http.router");
22
+ __JSKIT_FEATURE_ROUTE_SURFACE_NORMALIZER_LINE__
23
+ const routeBase = resolveScopedApiBasePath({
24
+ routeBase: "/",
25
+ relativePath: routeRelativePath,
26
+ strictParams: false
27
+ });
28
+
29
+ router.register(
30
+ "GET",
31
+ routeBase,
32
+ {
33
+ auth: "public",
34
+ __JSKIT_FEATURE_ROUTE_SURFACE_LINE__
35
+ meta: {
36
+ tags: ["feature"],
37
+ summary: "Read feature status."
38
+ },
39
+ query: statusQueryInputValidator
40
+ },
41
+ async function (request, reply) {
42
+ const response = await request.executeAction({
43
+ actionId: ACTION_GET_STATUS,
44
+ input: request.input.query || {}
45
+ });
46
+
47
+ reply.code(200).send(response);
48
+ }
49
+ );
50
+
51
+ router.register(
52
+ "POST",
53
+ routeBase,
54
+ {
55
+ auth: "public",
56
+ __JSKIT_FEATURE_ROUTE_SURFACE_LINE__
57
+ meta: {
58
+ tags: ["feature"],
59
+ summary: "Execute feature command."
60
+ },
61
+ body: executeCommandInputValidator
62
+ },
63
+ async function (request, reply) {
64
+ const response = await request.executeAction({
65
+ actionId: ACTION_EXECUTE,
66
+ input: request.input.body || {}
67
+ });
68
+
69
+ reply.code(200).send(response);
70
+ }
71
+ );
72
+ }
73
+
74
+ export { registerRoutes };
@@ -0,0 +1,39 @@
1
+ import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
2
+
3
+ const FEATURE_TABLE_NAME = "${option:feature-name|snake}";
4
+ const FEATURE_NAME = "${option:feature-name|kebab}";
5
+
6
+ function createRepository({ knex } = {}) {
7
+ if (!knex) {
8
+ throw new TypeError("createRepository requires knex.");
9
+ }
10
+
11
+ const withTransaction = createWithTransaction(knex);
12
+
13
+ return Object.freeze({
14
+ withTransaction,
15
+ async getStatus(input = {}, options = {}) {
16
+ return {
17
+ ok: true,
18
+ feature: FEATURE_NAME,
19
+ persistence: "custom-knex",
20
+ tableName: FEATURE_TABLE_NAME,
21
+ hasTransaction: Boolean(options?.trx),
22
+ input
23
+ };
24
+ },
25
+ async execute(input = {}, options = {}) {
26
+ return {
27
+ accepted: false,
28
+ feature: FEATURE_NAME,
29
+ persistence: "custom-knex",
30
+ tableName: FEATURE_TABLE_NAME,
31
+ hasTransaction: Boolean(options?.trx),
32
+ input,
33
+ message: "Customize repository.execute() with explicit knex queries."
34
+ };
35
+ }
36
+ });
37
+ }
38
+
39
+ export { createRepository };
@@ -0,0 +1,44 @@
1
+ import { createJsonRestContext } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
2
+
3
+ const FEATURE_RESOURCE_KEY = "${option:feature-name|camel}";
4
+ const FEATURE_RESOURCE_TYPE = "${option:feature-name|kebab}";
5
+
6
+ function createRepository({ api } = {}) {
7
+ if (!api) {
8
+ throw new TypeError("createRepository requires api.");
9
+ }
10
+
11
+ return Object.freeze({
12
+ async getStatus(input = {}, options = {}) {
13
+ const jsonRestContext = createJsonRestContext(options?.context || null);
14
+ const featureResource = api?.resources?.[FEATURE_RESOURCE_KEY] || null;
15
+
16
+ return {
17
+ ok: true,
18
+ feature: FEATURE_RESOURCE_TYPE,
19
+ persistence: "json-rest",
20
+ resourceKey: FEATURE_RESOURCE_KEY,
21
+ resourceAvailable: Boolean(featureResource),
22
+ hasContext: Boolean(jsonRestContext),
23
+ input
24
+ };
25
+ },
26
+ async execute(input = {}, options = {}) {
27
+ const jsonRestContext = createJsonRestContext(options?.context || null);
28
+ const featureResource = api?.resources?.[FEATURE_RESOURCE_KEY] || null;
29
+
30
+ return {
31
+ accepted: false,
32
+ feature: FEATURE_RESOURCE_TYPE,
33
+ persistence: "json-rest",
34
+ resourceKey: FEATURE_RESOURCE_KEY,
35
+ resourceAvailable: Boolean(featureResource),
36
+ hasContext: Boolean(jsonRestContext),
37
+ input,
38
+ message: "Customize repository.execute() to read and write through internal json-rest-api."
39
+ };
40
+ }
41
+ });
42
+ }
43
+
44
+ export { createRepository };
@@ -0,0 +1,14 @@
1
+ function createService({ featureRepository } = {}) {
2
+ __JSKIT_FEATURE_SERVICE_REPOSITORY_GUARD__
3
+
4
+ return Object.freeze({
5
+ async getStatus(input = {}, options = {}) {
6
+ __JSKIT_FEATURE_SERVICE_GET_STATUS_BODY__
7
+ },
8
+ async execute(input = {}, options = {}) {
9
+ __JSKIT_FEATURE_SERVICE_EXECUTE_BODY__
10
+ }
11
+ });
12
+ }
13
+
14
+ export { createService };
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildTemplateContext } from "../src/server/buildTemplateContext.js";
4
+
5
+ test("buildTemplateContext emits json-rest provider and descriptor wiring", async () => {
6
+ const context = await buildTemplateContext({
7
+ options: {
8
+ "feature-name": "Booking Engine",
9
+ mode: "json-rest"
10
+ }
11
+ });
12
+
13
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_DEPENDS_ON__, '"runtime.actions", "json-rest-api.core"');
14
+ assert.match(context.__JSKIT_FEATURE_PROVIDER_REPOSITORY_IMPORT__, /INTERNAL_JSON_REST_API/);
15
+ assert.match(context.__JSKIT_FEATURE_PROVIDER_REPOSITORY_REGISTRATION__, /feature\.booking-engine\.repository/);
16
+ assert.equal(
17
+ context.__JSKIT_FEATURE_PROVIDER_SERVICE_FACTORY_ARG__,
18
+ '{ featureRepository: _scope.make("feature.booking-engine.repository") }'
19
+ );
20
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_BOOT_METHOD__, " boot() {}");
21
+ assert.match(context.__JSKIT_FEATURE_DESCRIPTOR_DEPENDS_ON_LINES__, /@jskit-ai\/json-rest-api-core/);
22
+ assert.equal(context.__JSKIT_FEATURE_DESCRIPTOR_CAPABILITY_REQUIRES_LINES__, "");
23
+ assert.equal(context.__JSKIT_FEATURE_DESCRIPTOR_LANE__, "default");
24
+ });
25
+
26
+ test("buildTemplateContext emits orchestrator service placeholders and enabled-surface actions", async () => {
27
+ const context = await buildTemplateContext({
28
+ options: {
29
+ "feature-name": "Availability Engine",
30
+ mode: "orchestrator"
31
+ }
32
+ });
33
+
34
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_DEPENDS_ON__, '"runtime.actions"');
35
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_REPOSITORY_IMPORT__, "");
36
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_REPOSITORY_REGISTRATION__, "");
37
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_SERVICE_FACTORY_ARG__, "{}");
38
+ assert.equal(context.__JSKIT_FEATURE_ACTION_SURFACES_LINE__, ' surfacesFrom: "enabled",');
39
+ assert.match(context.__JSKIT_FEATURE_SERVICE_GET_STATUS_BODY__, /orchestration logic/);
40
+ assert.equal(context.__JSKIT_FEATURE_DESCRIPTOR_REPOSITORY_TOKEN_LINE__, "");
41
+ });
42
+
43
+ test("buildTemplateContext emits custom-knex route wiring and weird-custom lane metadata", async () => {
44
+ const context = await buildTemplateContext({
45
+ options: {
46
+ "feature-name": "Invoice Rollup",
47
+ mode: "custom-knex",
48
+ surface: "ADMIN",
49
+ "route-prefix": "Admin/Invoice Rollup"
50
+ }
51
+ });
52
+
53
+ assert.equal(context.__JSKIT_FEATURE_PROVIDER_DEPENDS_ON__, '"runtime.actions", "runtime.database"');
54
+ assert.match(context.__JSKIT_FEATURE_PROVIDER_REPOSITORY_IMPORT__, /createRepository/);
55
+ assert.match(context.__JSKIT_FEATURE_PROVIDER_BOOT_METHOD__, /routeRelativePath: "admin\/invoice-rollup"/);
56
+ assert.match(context.__JSKIT_FEATURE_PROVIDER_BOOT_METHOD__, /routeSurface: "admin"/);
57
+ assert.equal(context.__JSKIT_FEATURE_ACTION_SURFACES_LINE__, ' surfaces: ["admin"],');
58
+ assert.equal(context.__JSKIT_FEATURE_ROUTE_SURFACE_IMPORT__, ", normalizeSurfaceId");
59
+ assert.equal(context.__JSKIT_FEATURE_ROUTE_SURFACE_LINE__, " surface: normalizedRouteSurface,");
60
+ assert.equal(context.__JSKIT_FEATURE_DESCRIPTOR_LANE__, "weird-custom");
61
+ assert.equal(context.__JSKIT_FEATURE_DESCRIPTOR_CAPABILITY_REQUIRES_LINES__, "");
62
+ });
@@ -0,0 +1,53 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import descriptor from "../package.descriptor.mjs";
4
+
5
+ test("feature-server-generator exposes the scaffold primary subcommand contract", () => {
6
+ assert.equal(descriptor.kind, "generator");
7
+ assert.equal(descriptor.options?.mode?.validationType, "enum");
8
+ assert.deepEqual(
9
+ descriptor.options?.mode?.allowedValues,
10
+ ["json-rest", "orchestrator", "custom-knex"]
11
+ );
12
+ assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
13
+ assert.equal(descriptor.metadata?.generatorPrimarySubcommand, "scaffold");
14
+ assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.createTarget?.pathTemplate, "packages/${option:feature-name|kebab}");
15
+ assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("feature-name"), true);
16
+ assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("route-prefix"), true);
17
+ assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("force"), true);
18
+ });
19
+
20
+ test("feature-server-generator routes mode-specific files through mutation when clauses", () => {
21
+ const files = descriptor.mutations?.files || [];
22
+ const routesTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/registerRoutes.js");
23
+ const jsonRestRepositoryTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/repositoryJsonRest.js");
24
+ const customKnexRepositoryTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/repositoryCustomKnex.js");
25
+ const providerTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/FeatureProvider.js");
26
+ const actionIdsTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/actionIds.js");
27
+
28
+ assert.ok(routesTemplate);
29
+ assert.deepEqual(routesTemplate.when, {
30
+ option: "route-prefix",
31
+ hasText: true
32
+ });
33
+
34
+ assert.ok(jsonRestRepositoryTemplate);
35
+ assert.deepEqual(jsonRestRepositoryTemplate.when, {
36
+ option: "mode",
37
+ equals: "json-rest"
38
+ });
39
+
40
+ assert.ok(customKnexRepositoryTemplate);
41
+ assert.deepEqual(customKnexRepositoryTemplate.when, {
42
+ option: "mode",
43
+ equals: "custom-knex"
44
+ });
45
+
46
+ assert.ok(providerTemplate);
47
+ assert.deepEqual(providerTemplate.templateContext, {
48
+ entrypoint: "src/server/buildTemplateContext.js",
49
+ export: "buildTemplateContext"
50
+ });
51
+
52
+ assert.equal(actionIdsTemplate, undefined);
53
+ });