@jskit-ai/assistant 0.1.74 → 0.1.76

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/assistant",
4
- version: "0.1.74",
4
+ version: "0.1.76",
5
5
  kind: "generator",
6
6
  description: "Install assistant runtime/config for one surface and scaffold assistant pages at explicit target files.",
7
7
  options: {
@@ -49,14 +49,7 @@ export default Object.freeze({
49
49
  inputType: "text",
50
50
  defaultValue: "",
51
51
  promptLabel: "Link placement",
52
- promptHint: "Optional target for the generated page link placement (format: host:position)."
53
- },
54
- "link-component-token": {
55
- required: false,
56
- inputType: "text",
57
- defaultValue: "",
58
- promptLabel: "Link component token",
59
- promptHint: "Optional component token override for the generated page link placement."
52
+ promptHint: "Optional semantic target for the generated page link placement (format: area.slot)."
60
53
  },
61
54
  "link-to": {
62
55
  required: false,
@@ -169,7 +162,7 @@ export default Object.freeze({
169
162
  descriptionKey: "page-target-file"
170
163
  }
171
164
  ],
172
- optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
165
+ optionNames: ["name", "link-placement", "link-to", "force"],
173
166
  notes: [
174
167
  "The target file decides where the page lives.",
175
168
  "Page-link placement follows the same inference rules as ui-generator page.",
@@ -189,7 +182,7 @@ export default Object.freeze({
189
182
  "npx jskit generate assistant page \\",
190
183
  " admin/ops/copilot/index.vue \\",
191
184
  " --name \"Copilot\" \\",
192
- " --link-placement shell-layout:top-right"
185
+ " --link-placement shell.status"
193
186
  ]
194
187
  }
195
188
  ]
@@ -206,7 +199,7 @@ export default Object.freeze({
206
199
  descriptionKey: "page-target-file"
207
200
  }
208
201
  ],
209
- optionNames: ["surface", "name", "link-placement", "link-component-token", "link-to", "force"],
202
+ optionNames: ["surface", "name", "link-placement", "link-to", "force"],
210
203
  requiredOptionNames: ["surface"],
211
204
  notes: [
212
205
  "The target file decides where the settings page lives.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -9,6 +9,6 @@
9
9
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
10
10
  },
11
11
  "dependencies": {
12
- "@jskit-ai/kernel": "0.1.65"
12
+ "@jskit-ai/kernel": "0.1.67"
13
13
  }
14
14
  }
@@ -12,7 +12,10 @@ async function buildTemplateContext({ appRoot, options } = {}) {
12
12
  const settingsSurface = resolveSurfaceDefinition(appConfig, options?.["settings-surface"], "settings-surface");
13
13
  const configScope = normalizeConfigScope(options?.["config-scope"]);
14
14
 
15
- assertAssistantSurfaceIsAvailable(appConfig, runtimeSurface.id);
15
+ assertAssistantSurfaceIsAvailable(appConfig, runtimeSurface.id, {
16
+ settingsSurfaceId: settingsSurface.id,
17
+ configScope
18
+ });
16
19
 
17
20
  if (configScope === "workspace" && runtimeSurface.requiresWorkspace !== true) {
18
21
  throw new Error(
@@ -16,6 +16,14 @@ function resolveLinkToPropLine(linkTo = "") {
16
16
  return ` to: ${JSON.stringify(linkTo)},\n`;
17
17
  }
18
18
 
19
+ function resolveOwnerLine(owner = "") {
20
+ const normalizedOwner = normalizeText(owner);
21
+ if (!normalizedOwner) {
22
+ return "";
23
+ }
24
+ return ` owner: ${JSON.stringify(normalizedOwner)},\n`;
25
+ }
26
+
19
27
  function resolveTemplateFilePath(relativePath = "") {
20
28
  return fileURLToPath(new URL(relativePath, import.meta.url));
21
29
  }
@@ -49,7 +57,6 @@ async function resolveAssistantPageGenerationContext({
49
57
  targetFile,
50
58
  context,
51
59
  placement: options?.["link-placement"],
52
- componentToken: options?.["link-component-token"],
53
60
  linkTo: options?.["link-to"]
54
61
  });
55
62
 
@@ -57,7 +64,7 @@ async function resolveAssistantPageGenerationContext({
57
64
  pageTarget,
58
65
  pageLabel: normalizeText(options?.name) || pageTarget.defaultName,
59
66
  linkPlacementTarget: String(linkTarget.placementTarget?.id || ""),
60
- linkComponentToken: String(linkTarget.componentToken || ""),
67
+ linkOwnerLine: resolveOwnerLine(linkTarget.placementTarget?.owner || ""),
61
68
  linkWorkspaceSuffix: pageTarget.routeUrlSuffix,
62
69
  linkNonWorkspaceSuffix: pageTarget.routeUrlSuffix,
63
70
  linkWhenLine: String(linkTarget.whenLine || ""),
@@ -76,9 +83,10 @@ function renderAssistantPageLinkPlacementBlock({
76
83
  " addPlacement({\n" +
77
84
  ` id: "${String(pageTarget?.placementId || "")}",\n` +
78
85
  ` target: "${String(generationContext?.linkPlacementTarget || "")}",\n` +
86
+ `${String(generationContext?.linkOwnerLine || "")}` +
87
+ " kind: \"link\",\n" +
79
88
  ` surfaces: ["${String(pageTarget?.surfaceId || "")}"],\n` +
80
89
  " order: 155,\n" +
81
- ` componentToken: "${String(generationContext?.linkComponentToken || "")}",\n` +
82
90
  " props: {\n" +
83
91
  ` label: "${String(generationContext?.pageLabel || "")}",\n` +
84
92
  ` surface: "${String(pageTarget?.surfaceId || "")}",\n` +
@@ -30,7 +30,7 @@ async function runGeneratorSubcommand({
30
30
  }
31
31
 
32
32
  const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant page" });
33
- rejectUnexpectedOptions(options, ["name", "link-placement", "link-component-token", "link-to", "force"], {
33
+ rejectUnexpectedOptions(options, ["name", "link-placement", "link-to", "force"], {
34
34
  context: "assistant page"
35
35
  });
36
36
  const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
@@ -31,7 +31,7 @@ async function runGeneratorSubcommand({
31
31
  }
32
32
 
33
33
  const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant settings-page" });
34
- rejectUnexpectedOptions(options, ["surface", "name", "link-placement", "link-component-token", "link-to", "force"], {
34
+ rejectUnexpectedOptions(options, ["surface", "name", "link-placement", "link-to", "force"], {
35
35
  context: "assistant settings-page"
36
36
  });
37
37
  const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
@@ -49,11 +49,25 @@ function resolveSurfaceDefinition(appConfig = {}, surfaceId = "", optionName = "
49
49
  });
50
50
  }
51
51
 
52
- function assertAssistantSurfaceIsAvailable(appConfig = {}, surfaceId = "") {
52
+ function assertAssistantSurfaceIsAvailable(appConfig = {}, surfaceId = "", expected = {}) {
53
53
  const assistantSurfaces =
54
54
  appConfig && typeof appConfig.assistantSurfaces === "object" && !Array.isArray(appConfig.assistantSurfaces)
55
55
  ? appConfig.assistantSurfaces
56
56
  : {};
57
+ const existingSurface = assistantSurfaces[surfaceId];
58
+ if (!existingSurface) {
59
+ return;
60
+ }
61
+
62
+ const expectedSettingsSurfaceId = normalizeSurfaceId(expected?.settingsSurfaceId);
63
+ const expectedConfigScope = normalizeConfigScope(expected?.configScope);
64
+ const existingSettingsSurfaceId = normalizeSurfaceId(existingSurface?.settingsSurfaceId);
65
+ const existingConfigScope = normalizeConfigScope(existingSurface?.configScope);
66
+
67
+ if (existingSettingsSurfaceId === expectedSettingsSurfaceId && existingConfigScope === expectedConfigScope) {
68
+ return;
69
+ }
70
+
57
71
  if (assistantSurfaces[surfaceId]) {
58
72
  throw new Error(`assistant generator surface "${surfaceId}" already has an assistant configured in config/public.js.`);
59
73
  }
@@ -84,7 +84,7 @@ test("buildTemplateContext rejects workspace config scope for a non-workspace as
84
84
  });
85
85
  });
86
86
 
87
- test("buildTemplateContext rejects duplicate assistant surfaces already configured in public config", async () => {
87
+ test("buildTemplateContext accepts an already-configured matching assistant surface", async () => {
88
88
  await withTempApp(async (appRoot) => {
89
89
  await writeFile(
90
90
  path.join(appRoot, "config", "public.js"),
@@ -104,13 +104,48 @@ test("buildTemplateContext rejects duplicate assistant surfaces already configur
104
104
  "utf8"
105
105
  );
106
106
 
107
+ const context = await buildTemplateContext({
108
+ appRoot,
109
+ options: {
110
+ surface: "app",
111
+ "settings-surface": "console",
112
+ "config-scope": "global"
113
+ }
114
+ });
115
+
116
+ assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_ID__, "console");
117
+ assert.equal(context.__ASSISTANT_CONFIG_SCOPE__, "global");
118
+ });
119
+ });
120
+
121
+ test("buildTemplateContext rejects conflicting assistant surfaces already configured in public config", async () => {
122
+ await withTempApp(async (appRoot) => {
123
+ await writeFile(
124
+ path.join(appRoot, "config", "public.js"),
125
+ `export const config = {
126
+ surfaceDefinitions: {
127
+ app: { id: "app", enabled: true, requiresWorkspace: false, accessPolicyId: "authenticated" },
128
+ admin: { id: "admin", enabled: true, requiresWorkspace: true, accessPolicyId: "workspace_member" },
129
+ console: { id: "console", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" }
130
+ },
131
+ assistantSurfaces: {
132
+ app: {
133
+ settingsSurfaceId: "console",
134
+ configScope: "global"
135
+ }
136
+ }
137
+ };
138
+ `,
139
+ "utf8"
140
+ );
141
+
107
142
  await assert.rejects(
108
143
  () =>
109
144
  buildTemplateContext({
110
145
  appRoot,
111
146
  options: {
112
147
  surface: "app",
113
- "settings-surface": "console",
148
+ "settings-surface": "admin",
114
149
  "config-scope": "global"
115
150
  }
116
151
  }),
@@ -25,6 +25,61 @@ function toPagePath(targetFile = "") {
25
25
  return path.join("src/pages", targetFile);
26
26
  }
27
27
 
28
+ function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
29
+ const rendererLines = linkRenderer
30
+ ? `,
31
+ renderers: {
32
+ link: "${linkRenderer}"
33
+ }`
34
+ : "";
35
+ return `{
36
+ outlet: "${outlet}"${rendererLines}
37
+ }`;
38
+ }
39
+
40
+ function renderTopologyEntry({
41
+ id = "",
42
+ owner = "",
43
+ surfaces = ["*"],
44
+ defaultPlacement = false,
45
+ outlet = "",
46
+ linkRenderer = ""
47
+ } = {}) {
48
+ const ownerLine = owner ? ` owner: "${owner}",\n` : "";
49
+ const defaultLine = defaultPlacement ? " default: true,\n" : "";
50
+ return ` {
51
+ id: "${id}",
52
+ ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
53
+ ${defaultLine} variants: {
54
+ compact: ${renderTopologyVariant(outlet, { linkRenderer })},
55
+ medium: ${renderTopologyVariant(outlet, { linkRenderer })},
56
+ expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
57
+ }
58
+ }`;
59
+ }
60
+
61
+ async function writePlacementTopology(appRoot, entries = []) {
62
+ const defaultEntries = [
63
+ renderTopologyEntry({
64
+ id: "shell.primary-nav",
65
+ surfaces: ["*"],
66
+ defaultPlacement: true,
67
+ outlet: "shell-layout:primary-menu",
68
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
69
+ })
70
+ ];
71
+ await writeFileInApp(
72
+ appRoot,
73
+ "src/placementTopology.js",
74
+ `export default {
75
+ placements: [
76
+ ${[...defaultEntries, ...entries].join(",\n")}
77
+ ]
78
+ };
79
+ `
80
+ );
81
+ }
82
+
28
83
  async function writeAppFixture(appRoot, { configSource = "" } = {}) {
29
84
  await writeFileInApp(
30
85
  appRoot,
@@ -73,6 +128,7 @@ export default function getPlacements() {
73
128
  }
74
129
  `
75
130
  );
131
+ await writePlacementTopology(appRoot);
76
132
  }
77
133
 
78
134
  test("assistant page subcommand creates a runtime page at an explicit target file", async () => {
@@ -96,7 +152,9 @@ test("assistant page subcommand creates a runtime page at an explicit target fil
96
152
  const placementSource = await readFile(path.join(appRoot, "src/placement.js"), "utf8");
97
153
  assert.match(placementSource, /jskit:assistant\.page\.link:admin:\/ops\/copilot/);
98
154
  assert.match(placementSource, /id: "ui-generator\.page\.admin\.ops\.copilot\.link"/);
99
- assert.match(placementSource, /target: "shell-layout:primary-menu"/);
155
+ assert.match(placementSource, /target: "shell\.primary-nav"/);
156
+ assert.match(placementSource, /kind: "link"/);
157
+ assert.doesNotMatch(placementSource, /componentToken:/);
100
158
  assert.match(placementSource, /label: "Copilot"/);
101
159
  assert.match(placementSource, /scopedSuffix: "\/ops\/copilot"/);
102
160
  });
@@ -105,6 +163,15 @@ test("assistant page subcommand creates a runtime page at an explicit target fil
105
163
  test("assistant settings-page subcommand uses the target assistant surface and infers parent subpage placement", async () => {
106
164
  await withTempApp(async (appRoot) => {
107
165
  await writeAppFixture(appRoot);
166
+ await writePlacementTopology(appRoot, [
167
+ renderTopologyEntry({
168
+ id: "page.section-nav",
169
+ owner: "admin-settings",
170
+ surfaces: ["admin"],
171
+ outlet: "admin-settings:sub-pages",
172
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
173
+ })
174
+ ]);
108
175
  await writeFileInApp(
109
176
  appRoot,
110
177
  "src/pages/w/[workspaceSlug]/admin/settings/index.vue",
@@ -137,8 +204,9 @@ test("assistant settings-page subcommand uses the target assistant surface and i
137
204
 
138
205
  const placementSource = await readFile(path.join(appRoot, "src/placement.js"), "utf8");
139
206
  assert.match(placementSource, /jskit:assistant\.settings-page\.link:admin:\/settings\/assistant:console/);
140
- assert.match(placementSource, /target: "admin-settings:sub-pages"/);
141
- assert.match(placementSource, /componentToken: "local\.main\.ui\.surface-aware-menu-link-item"/);
207
+ assert.match(placementSource, /target: "page\.section-nav"/);
208
+ assert.match(placementSource, /owner: "admin-settings"/);
209
+ assert.doesNotMatch(placementSource, /componentToken: "local\.main\.ui\.surface-aware-menu-link-item"/);
142
210
  assert.match(placementSource, /to: "\.\/assistant"/);
143
211
  assert.match(placementSource, /label: "Assistant"/);
144
212
  });
@@ -241,6 +309,15 @@ test("assistant page subcommand overwrites an existing page when --force is pass
241
309
  test("assistant settings-page subcommand overwrites an existing page when --force is passed", async () => {
242
310
  await withTempApp(async (appRoot) => {
243
311
  await writeAppFixture(appRoot);
312
+ await writePlacementTopology(appRoot, [
313
+ renderTopologyEntry({
314
+ id: "page.section-nav",
315
+ owner: "admin-settings",
316
+ surfaces: ["admin"],
317
+ outlet: "admin-settings:sub-pages",
318
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
319
+ })
320
+ ]);
244
321
  await writeFileInApp(
245
322
  appRoot,
246
323
  "src/pages/w/[workspaceSlug]/admin/settings/index.vue",