@jskit-ai/ui-generator 0.1.14 → 0.1.15

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.
@@ -1,96 +1,91 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/ui-generator",
4
- version: "0.1.14",
4
+ version: "0.1.15",
5
5
  kind: "generator",
6
- description: "Generate app-local non-CRUD UI pages and outlet elements.",
6
+ description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
7
7
  options: {
8
8
  name: {
9
- required: true,
9
+ required: false,
10
10
  inputType: "text",
11
11
  defaultValue: "",
12
- promptLabel: "Element name",
13
- promptHint: "Display name and route slug source (example: Reports Dashboard)."
12
+ promptLabel: "Display label",
13
+ promptHint:
14
+ "Display label used for generated page links and named UI elements. For page, if omitted, it is derived from the target file path."
14
15
  },
15
16
  surface: {
16
- required: true,
17
+ required: false,
17
18
  inputType: "text",
19
+ validationType: "enabled-surface-id",
18
20
  defaultFromConfig: "surfaceDefaultId",
19
21
  promptLabel: "Target surface",
20
- promptHint: "Defaults to config.public.surfaceDefaultId. Must match an enabled surface id."
22
+ promptHint: "Used by the placed-element subcommand. Must match an enabled surface id."
21
23
  },
22
24
  path: {
23
25
  required: false,
24
26
  inputType: "text",
25
27
  defaultValue: "src/components",
26
28
  promptLabel: "Component path",
27
- promptHint: "Component directory relative to app root (used by element subcommand)."
29
+ promptHint: "Component directory relative to app root (used by placed-element and add-subpages support scaffold)."
28
30
  },
29
- "directory-prefix": {
31
+ force: {
30
32
  required: false,
31
- inputType: "text",
33
+ inputType: "flag",
32
34
  defaultValue: "",
33
- promptLabel: "Page directory prefix",
34
- promptHint: "Optional subpath under the selected surface pages root (example: crm or ops/team-a)."
35
- },
36
- "route-path": {
37
- required: false,
38
- inputType: "text",
39
- defaultValue: "",
40
- promptLabel: "Route path",
41
- promptHint:
42
- "Optional explicit container route path (example: contact-tools or contacts/[contactId]). Defaults to --name."
35
+ promptLabel: "Force overwrite",
36
+ promptHint: "Overwrite the generated file if it already exists."
43
37
  },
44
38
  placement: {
45
39
  required: false,
46
40
  inputType: "text",
47
41
  defaultValue: "",
48
42
  promptLabel: "Placement target",
49
- promptHint: "Optional host:position target (defaults to app ShellOutlet default target)."
43
+ promptHint: "Optional host:position target for placed-element placement (defaults to shell-layout:top-right)."
50
44
  },
51
- "placement-component-token": {
45
+ "link-placement": {
52
46
  required: false,
53
47
  inputType: "text",
54
48
  defaultValue: "",
55
- promptLabel: "Placement component token",
56
- promptHint:
57
- "Optional component token override for generated menu placement (example: local.main.ui.tab-link-item)."
49
+ promptLabel: "Link placement",
50
+ promptHint: "Optional host:position target for the generated page link placement."
58
51
  },
59
- "placement-to": {
52
+ "link-component-token": {
60
53
  required: false,
61
54
  inputType: "text",
62
55
  defaultValue: "",
63
- promptLabel: "Placement to",
56
+ promptLabel: "Link component token",
64
57
  promptHint:
65
- "Optional explicit props.to value for generated menu placement (example: ./notes). If omitted and directory-prefix includes a nestedChildren route group, defaults to ./<page-slug>."
58
+ "Optional component token override for the generated page link placement (example: local.main.ui.tab-link-item)."
66
59
  },
67
- file: {
60
+ "link-to": {
68
61
  required: false,
69
62
  inputType: "text",
70
63
  defaultValue: "",
71
- promptLabel: "Target Vue file",
72
- promptHint: "Vue SFC path relative to app root (used by outlet subcommand)."
64
+ promptLabel: "Link to",
65
+ promptHint:
66
+ "Optional explicit props.to value for the generated page link placement (example: ./notes). If omitted for pages under a detected parent subpages host, it is inferred from the page path."
73
67
  },
74
- host: {
68
+ target: {
75
69
  required: false,
76
70
  inputType: "text",
77
71
  defaultValue: "",
78
- promptLabel: "Outlet host",
79
- promptHint: "ShellOutlet host value to inject into target file."
72
+ promptLabel: "Outlet target",
73
+ promptHint:
74
+ "Used by add-subpages and outlet. Accepts host or host:position. If only host is given, position defaults to sub-pages."
80
75
  },
81
- position: {
76
+ title: {
82
77
  required: false,
83
78
  inputType: "text",
84
- defaultValue: "sub-pages",
85
- promptLabel: "Outlet position",
86
- promptHint: "ShellOutlet position value to inject into target file."
79
+ defaultValue: "",
80
+ promptLabel: "Section title",
81
+ promptHint: "Optional SectionContainerShell title override for add-subpages."
87
82
  },
88
- mode: {
83
+ subtitle: {
89
84
  required: false,
90
85
  inputType: "text",
91
- defaultValue: "routed",
92
- promptLabel: "Outlet mode",
93
- promptHint: "routed | outlet-only (routed injects RouterView when missing)."
86
+ defaultValue: "",
87
+ promptLabel: "Section subtitle",
88
+ promptHint: "Optional SectionContainerShell subtitle override for add-subpages."
94
89
  }
95
90
  },
96
91
  dependsOn: [],
@@ -110,27 +105,157 @@ export default Object.freeze({
110
105
  generatorPrimarySubcommand: "page",
111
106
  generatorSubcommands: {
112
107
  page: {
113
- description: "Scaffold a non-CRUD page and add a menu placement entry.",
114
- optionNames: ["name", "surface", "directory-prefix", "placement", "placement-component-token", "placement-to"]
108
+ entrypoint: "src/server/subcommands/page.js",
109
+ export: "runGeneratorSubcommand",
110
+ description: "Create a route page at an explicit target file and add a link placement entry for it.",
111
+ longDescription: [
112
+ "This command always creates one route page file. By default, its page link is placed from the page path itself.",
113
+ "If an ancestor page has already been enhanced with sub-pages, JSKIT treats that ancestor as the real host. In that case the new page is linked into the nearest parent sub-pages outlet instead of the shell menu.",
114
+ "That means the generated link normally becomes a tab or child-page link under that ancestor host, and `props.to` is inferred relative to that host. If the host page is `index.vue`, child pages belong under `index/...` so the router keeps the parent page visible while the child route renders underneath it."
115
+ ],
116
+ positionalArgs: [
117
+ {
118
+ name: "target-file",
119
+ required: true,
120
+ descriptionKey: "page-target-file"
121
+ }
122
+ ],
123
+ optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
124
+ notes: [
125
+ "If a nearest parent subpages host is found, placement, link component token, and props.to are inferred automatically.",
126
+ "If the parent host page is index.vue, child pages belong under index/...",
127
+ "If the target page file already exists, rerun with --force to overwrite it."
128
+ ],
129
+ examples: [
130
+ {
131
+ label: "Common usage",
132
+ lines: [
133
+ "npx jskit generate ui-generator page \\",
134
+ " admin/reports/index.vue \\",
135
+ " --name \"Reports\""
136
+ ]
137
+ },
138
+ {
139
+ label: "More advanced usage",
140
+ lines: [
141
+ "npx jskit generate ui-generator page \\",
142
+ " admin/customers/[customerId]/index/notes/index.vue \\",
143
+ " --name \"Notes\" \\",
144
+ " --force"
145
+ ]
146
+ }
147
+ ]
115
148
  },
116
- element: {
149
+ "placed-element": {
117
150
  entrypoint: "src/server/subcommands/element.js",
118
151
  export: "runGeneratorSubcommand",
119
- description: "Scaffold a reusable UI element component and register a placement.",
120
- optionNames: ["name", "surface", "path", "placement"]
152
+ description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
153
+ optionNames: ["name", "surface", "path", "placement", "force"],
154
+ requiredOptionNames: ["name", "surface"],
155
+ notes: [
156
+ "If --placement is omitted, the placed element is added at shell-layout:top-right.",
157
+ "If the component file already exists, rerun with --force to overwrite it."
158
+ ],
159
+ examples: [
160
+ {
161
+ label: "Common usage",
162
+ lines: [
163
+ "npx jskit generate ui-generator placed-element \\",
164
+ " --name \"Alerts Widget\" \\",
165
+ " --surface admin"
166
+ ]
167
+ },
168
+ {
169
+ label: "More advanced usage",
170
+ lines: [
171
+ "npx jskit generate ui-generator placed-element \\",
172
+ " --name \"Ops Panel\" \\",
173
+ " --surface admin \\",
174
+ " --path src/widgets \\",
175
+ " --placement shell-layout:top-right \\",
176
+ " --force"
177
+ ]
178
+ }
179
+ ]
121
180
  },
122
- container: {
123
- entrypoint: "src/server/subcommands/container.js",
181
+ "add-subpages": {
182
+ entrypoint: "src/server/subcommands/addSubpages.js",
124
183
  export: "runGeneratorSubcommand",
125
- description: "Scaffold a routed section container page with a tab outlet. Adds a menu entry only when --placement is passed.",
126
- optionNames: ["name", "surface", "directory-prefix", "route-path", "path", "placement"]
184
+ description: "Upgrade an existing page into a routed subpage host with SectionContainerShell, ShellOutlet, and RouterView.",
185
+ positionalArgs: [
186
+ {
187
+ name: "target-file",
188
+ required: true,
189
+ descriptionKey: "existing-page-target-file"
190
+ }
191
+ ],
192
+ optionNames: ["target", "path", "title", "subtitle"],
193
+ notes: [
194
+ "Use this when the page should render shared content plus child routes below it.",
195
+ "If the host page is index.vue, create child pages under index/..."
196
+ ],
197
+ examples: [
198
+ {
199
+ label: "Common usage",
200
+ lines: [
201
+ "npx jskit generate ui-generator add-subpages \\",
202
+ " admin/customers/[customerId]/index.vue \\",
203
+ " --title \"Customer\" \\",
204
+ " --subtitle \"View and manage this customer.\""
205
+ ]
206
+ },
207
+ {
208
+ label: "More advanced usage",
209
+ lines: [
210
+ "npx jskit generate ui-generator add-subpages \\",
211
+ " admin/contacts/[contactId]/index.vue \\",
212
+ " --target contact-view:summary-tabs \\",
213
+ " --path src/components/admin \\",
214
+ " --title \"Contact\" \\",
215
+ " --subtitle \"Manage contact modules.\""
216
+ ]
217
+ }
218
+ ]
127
219
  },
128
220
  outlet: {
129
221
  entrypoint: "src/server/subcommands/outlet.js",
130
222
  export: "runGeneratorSubcommand",
131
- description: "Inject a ShellOutlet block into an existing Vue page/component.",
132
- optionNames: ["file", "host", "position", "mode"],
133
- requiredOptionNames: ["file", "host"]
223
+ description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
224
+ longDescription: [
225
+ "A ShellOutlet creates a named placement target inside a Vue file. That target is what other parts of JSKIT render into later.",
226
+ "After an outlet exists, `jskit list-placements` will discover it and show it as `host:position`. That makes the target visible to humans and to generators that need a placement destination.",
227
+ "Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same `host:position`."
228
+ ],
229
+ positionalArgs: [
230
+ {
231
+ name: "target-file",
232
+ required: true,
233
+ descriptionKey: "existing-vue-sfc-target-file"
234
+ }
235
+ ],
236
+ optionNames: ["target"],
237
+ requiredOptionNames: ["target"],
238
+ notes: [
239
+ "Use --target host or --target host:position. If only host is given, position defaults to sub-pages."
240
+ ],
241
+ examples: [
242
+ {
243
+ label: "Common usage",
244
+ lines: [
245
+ "npx jskit generate ui-generator outlet \\",
246
+ " src/components/ContactSummaryCard.vue \\",
247
+ " --target contact-view"
248
+ ]
249
+ },
250
+ {
251
+ label: "More advanced usage",
252
+ lines: [
253
+ "npx jskit generate ui-generator outlet \\",
254
+ " src/pages/admin/customers/[customerId]/index.vue \\",
255
+ " --target customer-view:summary-actions"
256
+ ]
257
+ }
258
+ ]
134
259
  }
135
260
  },
136
261
  apiSummary: {
@@ -149,7 +274,7 @@ export default Object.freeze({
149
274
  mutations: {
150
275
  dependencies: {
151
276
  runtime: {
152
- "@jskit-ai/users-web": "0.1.46"
277
+ "@jskit-ai/users-web": "0.1.47"
153
278
  },
154
279
  dev: {}
155
280
  },
@@ -157,32 +282,7 @@ export default Object.freeze({
157
282
  scripts: {}
158
283
  },
159
284
  procfile: {},
160
- files: [
161
- {
162
- from: "templates/src/pages/admin/ui-generator/Page.vue",
163
- toSurface: "${option:surface|lower}",
164
- toSurfacePath: "${option:directory-prefix|pathprefix}${option:name|path}/index.vue",
165
- reason: "Install generated UI page scaffold.",
166
- category: "ui-generator",
167
- id: "ui-generator-page-${option:name|snake}"
168
- }
169
- ],
170
- text: [
171
- {
172
- op: "append-text",
173
- file: "src/placement.js",
174
- position: "bottom",
175
- skipIfContains: "jskit:ui-generator.page.menu:${option:surface|lower}:${option:directory-prefix|path}:${option:name|path}",
176
- value:
177
- "\n// jskit:ui-generator.page.menu:${option:surface|lower}:${option:directory-prefix|path}:${option:name|path}\n{\n addPlacement({\n id: \"__JSKIT_UI_MENU_PLACEMENT_ID__\",\n host: \"__JSKIT_UI_MENU_PLACEMENT_HOST__\",\n position: \"__JSKIT_UI_MENU_PLACEMENT_POSITION__\",\n surfaces: [\"${option:surface|lower}\"],\n order: 155,\n componentToken: \"__JSKIT_UI_MENU_COMPONENT_TOKEN__\",\n props: {\n label: \"${option:name|trim}\",\n surface: \"${option:surface|lower}\",\n workspaceSuffix: \"__JSKIT_UI_MENU_WORKSPACE_SUFFIX__\",\n nonWorkspaceSuffix: \"__JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__\",\n__JSKIT_UI_MENU_TO_PROP_LINE__ },\n when: ({ auth }) => Boolean(auth?.authenticated)\n });\n}\n",
178
- reason: "Append generated UI page menu placement.",
179
- category: "ui-generator",
180
- id: "ui-generator-page-placement-menu-${option:name|snake}",
181
- templateContext: {
182
- entrypoint: "src/server/buildTemplateContext.js",
183
- export: "buildUiPageTemplateContext"
184
- }
185
- }
186
- ]
285
+ files: [],
286
+ text: []
187
287
  }
188
288
  });
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@jskit-ai/ui-generator",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/kernel": "0.1.31"
9
+ "@jskit-ai/kernel": "0.1.32"
10
10
  },
11
11
  "exports": {
12
12
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -1,148 +1,43 @@
1
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { resolveShellOutletPlacementTargetFromApp } from "@jskit-ai/kernel/server/support";
1
+ import {
2
+ resolvePageLinkTargetDetails,
3
+ resolvePageTargetDetails
4
+ } from "@jskit-ai/kernel/server/support";
3
5
 
4
- const DEFAULT_MENU_COMPONENT_TOKEN = "users.web.shell.surface-aware-menu-link-item";
5
- const NESTED_CHILDREN_GROUPS = new Set(["nestedchildren", "nested-children"]);
6
-
7
- function splitTextIntoWords(value = "") {
8
- const normalized = String(value || "")
9
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
10
- .replace(/[^A-Za-z0-9]+/g, " ")
11
- .trim();
12
- if (!normalized) {
13
- return [];
14
- }
15
- return normalized
16
- .split(/\s+/)
17
- .map((entry) => entry.toLowerCase())
18
- .filter(Boolean);
19
- }
20
-
21
- function wordsToKebab(words = []) {
22
- return (Array.isArray(words) ? words : [])
23
- .map((entry) => String(entry || "").toLowerCase())
24
- .filter(Boolean)
25
- .join("-");
26
- }
27
-
28
- function normalizePathValue(value = "") {
29
- return String(value || "")
30
- .replaceAll("\\", "/")
31
- .split("/")
32
- .map((segment) => {
33
- const normalizedSegment = normalizeText(segment);
34
- if (!normalizedSegment) {
35
- return "";
36
- }
37
- if (/^\[[^\]]+\]$/.test(normalizedSegment)) {
38
- return normalizedSegment;
39
- }
40
- const routeGroupMatch = /^\(([^()]+)\)$/.exec(normalizedSegment);
41
- if (routeGroupMatch) {
42
- const routeGroupName = wordsToKebab(splitTextIntoWords(routeGroupMatch[1]));
43
- return routeGroupName ? `(${routeGroupName})` : "";
44
- }
45
- return wordsToKebab(splitTextIntoWords(normalizedSegment));
46
- })
47
- .filter(Boolean)
48
- .join("/");
49
- }
50
-
51
- function splitPathSegments(value = "") {
52
- return normalizePathValue(value)
53
- .split("/")
54
- .map((entry) => normalizeText(entry))
55
- .filter(Boolean);
56
- }
57
-
58
- function isRouteGroupSegment(value = "") {
59
- const source = normalizeText(value);
60
- return source.startsWith("(") && source.endsWith(")");
61
- }
62
-
63
- function isNestedChildrenRouteGroupSegment(value = "") {
64
- const source = normalizeText(value);
65
- if (!isRouteGroupSegment(source)) {
66
- return false;
67
- }
68
- const groupName = source.slice(1, -1).trim().toLowerCase();
69
- return NESTED_CHILDREN_GROUPS.has(groupName);
70
- }
71
-
72
- function resolvePlacementUrlSuffix(options = {}) {
73
- const routeSegments = [
74
- ...splitPathSegments(options?.["directory-prefix"]),
75
- ...splitPathSegments(options?.name)
76
- ].filter((segment) => !isRouteGroupSegment(segment));
77
- if (routeSegments.length < 1) {
78
- return "/";
79
- }
80
- return `/${routeSegments.join("/")}`;
81
- }
82
-
83
- function resolveMenuComponentToken(options = {}) {
84
- const explicitToken = normalizeText(options?.["placement-component-token"]);
85
- if (explicitToken) {
86
- return explicitToken;
87
- }
88
- return DEFAULT_MENU_COMPONENT_TOKEN;
89
- }
90
-
91
- function resolveAutoRelativePlacementTo(options = {}) {
92
- const explicitPlacementTo = normalizeText(options?.["placement-to"]);
93
- if (explicitPlacementTo) {
94
- return explicitPlacementTo;
95
- }
96
- const directorySegments = splitPathSegments(options?.["directory-prefix"]);
97
- const hasNestedChildrenGroup = directorySegments.some((segment) => isNestedChildrenRouteGroupSegment(segment));
98
- if (!hasNestedChildrenGroup) {
6
+ function resolveLinkToPropLine(linkTo = "") {
7
+ if (!linkTo) {
99
8
  return "";
100
9
  }
101
- const pagePath = normalizePathValue(options?.name);
102
- if (!pagePath) {
103
- return "";
104
- }
105
- return `./${pagePath}`;
10
+ return ` to: ${JSON.stringify(linkTo)},\n`;
106
11
  }
107
12
 
108
- function resolveMenuToPropLine(options = {}) {
109
- const placementTo = resolveAutoRelativePlacementTo(options);
110
- if (!placementTo) {
111
- return "";
112
- }
113
- return ` to: ${JSON.stringify(placementTo)},\n`;
114
- }
115
-
116
- function normalizePlacementIdSegment(value = "") {
117
- return wordsToKebab(splitTextIntoWords(value));
118
- }
119
-
120
- function resolveMenuPlacementId(options = {}) {
121
- const idSegments = [
122
- ...splitPathSegments(options?.["directory-prefix"]),
123
- ...splitPathSegments(options?.name)
124
- ]
125
- .map((segment) => normalizePlacementIdSegment(segment))
126
- .filter(Boolean);
127
-
128
- return `ui-generator.page.${idSegments.join(".")}.menu`;
129
- }
130
-
131
- async function buildUiPageTemplateContext({ appRoot, options } = {}) {
132
- const placementTarget = await resolveShellOutletPlacementTargetFromApp({
13
+ async function buildUiPageTemplateContext({
14
+ appRoot,
15
+ targetFile = "",
16
+ options = {}
17
+ } = {}) {
18
+ const pageTarget = await resolvePageTargetDetails({
133
19
  appRoot,
134
- context: "ui-generator",
135
- placement: options?.placement
20
+ targetFile,
21
+ context: "ui-generator page"
22
+ });
23
+ const linkTarget = await resolvePageLinkTargetDetails({
24
+ appRoot: pageTarget.appRoot,
25
+ pageTarget,
26
+ targetFile,
27
+ context: "ui-generator page",
28
+ placement: options?.["link-placement"],
29
+ componentToken: options?.["link-component-token"],
30
+ linkTo: options?.["link-to"]
136
31
  });
137
32
 
138
33
  return {
139
- __JSKIT_UI_MENU_PLACEMENT_ID__: resolveMenuPlacementId(options),
140
- __JSKIT_UI_MENU_PLACEMENT_HOST__: normalizeText(placementTarget?.host),
141
- __JSKIT_UI_MENU_PLACEMENT_POSITION__: normalizeText(placementTarget?.position),
142
- __JSKIT_UI_MENU_COMPONENT_TOKEN__: resolveMenuComponentToken(options),
143
- __JSKIT_UI_MENU_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
144
- __JSKIT_UI_MENU_NON_WORKSPACE_SUFFIX__: resolvePlacementUrlSuffix(options),
145
- __JSKIT_UI_MENU_TO_PROP_LINE__: resolveMenuToPropLine(options)
34
+ __JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
35
+ __JSKIT_UI_LINK_PLACEMENT_HOST__: String(linkTarget.placementTarget?.host || ""),
36
+ __JSKIT_UI_LINK_PLACEMENT_POSITION__: String(linkTarget.placementTarget?.position || ""),
37
+ __JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
38
+ __JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
39
+ __JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
40
+ __JSKIT_UI_LINK_TO_PROP_LINE__: resolveLinkToPropLine(linkTarget.linkTo)
146
41
  };
147
42
  }
148
43
 
@@ -0,0 +1,73 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import {
3
+ DEFAULT_COMPONENT_DIRECTORY,
4
+ DEFAULT_SUBPAGES_POSITION,
5
+ deriveDefaultSubpagesHost,
6
+ resolvePageTargetDetails,
7
+ upgradePageFileToSubpages
8
+ } from "./pageSupport.js";
9
+ import {
10
+ requireSinglePositionalTargetFile,
11
+ resolveOutletTargetId,
12
+ rejectUnexpectedOptions
13
+ } from "./support.js";
14
+
15
+ function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
16
+ const rawTarget = normalizeText(options?.target);
17
+ return resolveOutletTargetId(rawTarget || deriveDefaultSubpagesHost(pageTarget), {
18
+ context: "ui-generator add-subpages",
19
+ optionName: "target",
20
+ defaultPosition: DEFAULT_SUBPAGES_POSITION
21
+ });
22
+ }
23
+
24
+ async function runGeneratorSubcommand({
25
+ appRoot,
26
+ subcommand = "",
27
+ args = [],
28
+ options = {},
29
+ dryRun = false
30
+ } = {}) {
31
+ const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
32
+ if (normalizedSubcommand !== "add-subpages") {
33
+ throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
34
+ }
35
+ const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator add-subpages" });
36
+ rejectUnexpectedOptions(
37
+ options,
38
+ ["target", "path", "title", "subtitle"],
39
+ { context: "ui-generator add-subpages" }
40
+ );
41
+
42
+ const componentDirectory = normalizeText(options?.path) || DEFAULT_COMPONENT_DIRECTORY;
43
+ const title = normalizeText(options?.title);
44
+ const subtitle = normalizeText(options?.subtitle);
45
+ const pageTarget = await resolvePageTargetDetails({
46
+ appRoot,
47
+ targetFile,
48
+ context: "ui-generator add-subpages"
49
+ });
50
+ const outletTarget = resolveSubpagesOutletTarget(options, pageTarget);
51
+
52
+ const result = await upgradePageFileToSubpages({
53
+ appRoot,
54
+ targetFile,
55
+ host: outletTarget.host,
56
+ position: outletTarget.position,
57
+ title,
58
+ subtitle,
59
+ componentDirectory,
60
+ preserveExistingContent: true,
61
+ dryRun
62
+ });
63
+
64
+ return {
65
+ touchedFiles: result.touchedFiles,
66
+ summary:
67
+ result.touchedFiles.length > 0
68
+ ? `Enabled subpages in ${result.targetFile} for "${pageTarget.routeUrlSuffix}" using outlet target "${outletTarget.id}".`
69
+ : `Subpages are already enabled in ${result.targetFile}.`
70
+ };
71
+ }
72
+
73
+ export { runGeneratorSubcommand };