@jskit-ai/assistant 0.1.40 → 0.1.42
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.
- package/README.md +91 -0
- package/package.descriptor.mjs +193 -117
- package/package.json +2 -2
- package/src/server/buildTemplateContext.js +7 -104
- package/src/server/pageSupport.js +114 -0
- package/src/server/subcommands/page.js +104 -0
- package/src/server/subcommands/settingsPage.js +108 -0
- package/src/server/subcommands/support.js +108 -0
- package/src/server/support.js +78 -0
- package/test/buildTemplateContext.test.js +20 -27
- package/test/packageDescriptor.test.js +41 -26
- package/test/subcommands.test.js +285 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
resolvePageLinkTargetDetails,
|
|
5
|
+
resolvePageTargetDetails
|
|
6
|
+
} from "@jskit-ai/kernel/server/support";
|
|
7
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
8
|
+
|
|
9
|
+
const PAGE_TEMPLATE_FILE = "../../templates/src/pages/assistant/index.vue";
|
|
10
|
+
const SETTINGS_PAGE_TEMPLATE_FILE = "../../templates/src/pages/settings/assistant/index.vue";
|
|
11
|
+
|
|
12
|
+
function resolveLinkToPropLine(linkTo = "") {
|
|
13
|
+
if (!linkTo) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
return ` to: ${JSON.stringify(linkTo)},\n`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveTemplateFilePath(relativePath = "") {
|
|
20
|
+
return fileURLToPath(new URL(relativePath, import.meta.url));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readAssistantPageTemplateSource(kind = "page") {
|
|
24
|
+
const templateFilePath =
|
|
25
|
+
kind === "settings-page"
|
|
26
|
+
? resolveTemplateFilePath(SETTINGS_PAGE_TEMPLATE_FILE)
|
|
27
|
+
: resolveTemplateFilePath(PAGE_TEMPLATE_FILE);
|
|
28
|
+
return readFile(templateFilePath, "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderAssistantPageSource(templateSource = "", surfaceId = "") {
|
|
32
|
+
return String(templateSource || "").replaceAll("__ASSISTANT_SURFACE_ID__", String(surfaceId || ""));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function resolveAssistantPageGenerationContext({
|
|
36
|
+
appRoot,
|
|
37
|
+
targetFile = "",
|
|
38
|
+
options = {},
|
|
39
|
+
context = "assistant page"
|
|
40
|
+
} = {}) {
|
|
41
|
+
const pageTarget = await resolvePageTargetDetails({
|
|
42
|
+
appRoot,
|
|
43
|
+
targetFile,
|
|
44
|
+
context
|
|
45
|
+
});
|
|
46
|
+
const linkTarget = await resolvePageLinkTargetDetails({
|
|
47
|
+
appRoot: pageTarget.appRoot,
|
|
48
|
+
pageTarget,
|
|
49
|
+
targetFile,
|
|
50
|
+
context,
|
|
51
|
+
placement: options?.["link-placement"],
|
|
52
|
+
componentToken: options?.["link-component-token"],
|
|
53
|
+
linkTo: options?.["link-to"]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
pageTarget,
|
|
58
|
+
pageLabel: normalizeText(options?.name) || pageTarget.defaultName,
|
|
59
|
+
linkPlacementHost: String(linkTarget.placementTarget?.host || ""),
|
|
60
|
+
linkPlacementPosition: String(linkTarget.placementTarget?.position || ""),
|
|
61
|
+
linkComponentToken: String(linkTarget.componentToken || ""),
|
|
62
|
+
linkWorkspaceSuffix: pageTarget.routeUrlSuffix,
|
|
63
|
+
linkNonWorkspaceSuffix: pageTarget.routeUrlSuffix,
|
|
64
|
+
linkToPropLine: resolveLinkToPropLine(linkTarget.linkTo)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderAssistantPageLinkPlacementBlock({
|
|
69
|
+
marker = "",
|
|
70
|
+
pageTarget = {},
|
|
71
|
+
generationContext = {}
|
|
72
|
+
} = {}) {
|
|
73
|
+
return (
|
|
74
|
+
`// ${marker}\n` +
|
|
75
|
+
"{\n" +
|
|
76
|
+
" addPlacement({\n" +
|
|
77
|
+
` id: "${String(pageTarget?.placementId || "")}",\n` +
|
|
78
|
+
` host: "${String(generationContext?.linkPlacementHost || "")}",\n` +
|
|
79
|
+
` position: "${String(generationContext?.linkPlacementPosition || "")}",\n` +
|
|
80
|
+
` surfaces: ["${String(pageTarget?.surfaceId || "")}"],\n` +
|
|
81
|
+
" order: 155,\n" +
|
|
82
|
+
` componentToken: "${String(generationContext?.linkComponentToken || "")}",\n` +
|
|
83
|
+
" props: {\n" +
|
|
84
|
+
` label: "${String(generationContext?.pageLabel || "")}",\n` +
|
|
85
|
+
` surface: "${String(pageTarget?.surfaceId || "")}",\n` +
|
|
86
|
+
` workspaceSuffix: "${String(generationContext?.linkWorkspaceSuffix || "")}",\n` +
|
|
87
|
+
` nonWorkspaceSuffix: "${String(generationContext?.linkNonWorkspaceSuffix || "")}",\n` +
|
|
88
|
+
`${String(generationContext?.linkToPropLine || "")} },\n` +
|
|
89
|
+
" when: ({ auth }) => Boolean(auth?.authenticated)\n" +
|
|
90
|
+
" });\n" +
|
|
91
|
+
"}\n"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderAssistantPageSummary(
|
|
96
|
+
pageTarget = {},
|
|
97
|
+
{ pageAlreadyExisted = false, pageOverwritten = false } = {}
|
|
98
|
+
) {
|
|
99
|
+
if (!pageAlreadyExisted) {
|
|
100
|
+
return `Generated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
|
|
101
|
+
}
|
|
102
|
+
if (pageOverwritten) {
|
|
103
|
+
return `Regenerated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
|
|
104
|
+
}
|
|
105
|
+
return `Generated assistant page "${String(pageTarget?.routeUrlSuffix || "")}".`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
readAssistantPageTemplateSource,
|
|
110
|
+
renderAssistantPageSource,
|
|
111
|
+
resolveAssistantPageGenerationContext,
|
|
112
|
+
renderAssistantPageLinkPlacementBlock,
|
|
113
|
+
renderAssistantPageSummary
|
|
114
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import {
|
|
5
|
+
readAssistantPageTemplateSource,
|
|
6
|
+
renderAssistantPageLinkPlacementBlock,
|
|
7
|
+
renderAssistantPageSource,
|
|
8
|
+
renderAssistantPageSummary,
|
|
9
|
+
resolveAssistantPageGenerationContext
|
|
10
|
+
} from "../pageSupport.js";
|
|
11
|
+
import {
|
|
12
|
+
PLACEMENT_FILE,
|
|
13
|
+
appendBlockIfMarkerMissing,
|
|
14
|
+
rejectUnexpectedOptions,
|
|
15
|
+
requireEmptyPageSource,
|
|
16
|
+
requireSinglePositionalTargetFile,
|
|
17
|
+
resolvePathWithinApp
|
|
18
|
+
} from "./support.js";
|
|
19
|
+
|
|
20
|
+
async function runGeneratorSubcommand({
|
|
21
|
+
appRoot,
|
|
22
|
+
subcommand = "",
|
|
23
|
+
args = [],
|
|
24
|
+
options = {},
|
|
25
|
+
dryRun = false
|
|
26
|
+
} = {}) {
|
|
27
|
+
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
28
|
+
if (normalizedSubcommand !== "page") {
|
|
29
|
+
throw new Error(`Unsupported assistant subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant page" });
|
|
33
|
+
rejectUnexpectedOptions(options, ["name", "link-placement", "link-component-token", "link-to", "force"], {
|
|
34
|
+
context: "assistant page"
|
|
35
|
+
});
|
|
36
|
+
const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
|
|
37
|
+
? normalizeBoolean(options.force)
|
|
38
|
+
: false;
|
|
39
|
+
|
|
40
|
+
const generationContext = await resolveAssistantPageGenerationContext({
|
|
41
|
+
appRoot,
|
|
42
|
+
targetFile,
|
|
43
|
+
options,
|
|
44
|
+
context: "assistant page"
|
|
45
|
+
});
|
|
46
|
+
const pageTarget = generationContext.pageTarget;
|
|
47
|
+
const pageFilePath = pageTarget.targetFilePath.absolutePath;
|
|
48
|
+
const pageRelativePath = pageTarget.targetFilePath.relativePath;
|
|
49
|
+
const templateSource = await readAssistantPageTemplateSource("page");
|
|
50
|
+
const desiredPageSource = renderAssistantPageSource(templateSource, pageTarget.surfaceId);
|
|
51
|
+
|
|
52
|
+
let existingPageSource = "";
|
|
53
|
+
let pageAlreadyExisted = true;
|
|
54
|
+
try {
|
|
55
|
+
existingPageSource = await readFile(pageFilePath, "utf8");
|
|
56
|
+
} catch {
|
|
57
|
+
pageAlreadyExisted = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
requireEmptyPageSource(existingPageSource, pageRelativePath, {
|
|
61
|
+
context: "assistant page",
|
|
62
|
+
forceOverwrite
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const touchedFiles = new Set();
|
|
66
|
+
if (!pageAlreadyExisted || forceOverwrite) {
|
|
67
|
+
if (dryRun !== true) {
|
|
68
|
+
await mkdir(path.dirname(pageFilePath), { recursive: true });
|
|
69
|
+
await writeFile(pageFilePath, desiredPageSource, "utf8");
|
|
70
|
+
}
|
|
71
|
+
touchedFiles.add(pageRelativePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
75
|
+
context: "assistant page"
|
|
76
|
+
});
|
|
77
|
+
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
78
|
+
const placementMarker = `jskit:assistant.page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}`;
|
|
79
|
+
const placementApplied = appendBlockIfMarkerMissing(
|
|
80
|
+
placementSource,
|
|
81
|
+
placementMarker,
|
|
82
|
+
renderAssistantPageLinkPlacementBlock({
|
|
83
|
+
marker: placementMarker,
|
|
84
|
+
pageTarget,
|
|
85
|
+
generationContext
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
if (placementApplied.changed) {
|
|
89
|
+
if (dryRun !== true) {
|
|
90
|
+
await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
|
|
91
|
+
}
|
|
92
|
+
touchedFiles.add(placementPath.relativePath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
97
|
+
summary: renderAssistantPageSummary(pageTarget, {
|
|
98
|
+
pageAlreadyExisted,
|
|
99
|
+
pageOverwritten: pageAlreadyExisted && forceOverwrite
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { runGeneratorSubcommand };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { normalizeBoolean, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import {
|
|
5
|
+
readAssistantPageTemplateSource,
|
|
6
|
+
renderAssistantPageLinkPlacementBlock,
|
|
7
|
+
renderAssistantPageSource,
|
|
8
|
+
renderAssistantPageSummary,
|
|
9
|
+
resolveAssistantPageGenerationContext
|
|
10
|
+
} from "../pageSupport.js";
|
|
11
|
+
import { loadAppConfig, resolveSurfaceDefinition } from "../support.js";
|
|
12
|
+
import {
|
|
13
|
+
PLACEMENT_FILE,
|
|
14
|
+
appendBlockIfMarkerMissing,
|
|
15
|
+
rejectUnexpectedOptions,
|
|
16
|
+
requireEmptyPageSource,
|
|
17
|
+
requireSinglePositionalTargetFile,
|
|
18
|
+
resolvePathWithinApp
|
|
19
|
+
} from "./support.js";
|
|
20
|
+
|
|
21
|
+
async function runGeneratorSubcommand({
|
|
22
|
+
appRoot,
|
|
23
|
+
subcommand = "",
|
|
24
|
+
args = [],
|
|
25
|
+
options = {},
|
|
26
|
+
dryRun = false
|
|
27
|
+
} = {}) {
|
|
28
|
+
const normalizedSubcommand = normalizeText(subcommand).toLowerCase();
|
|
29
|
+
if (normalizedSubcommand !== "settings-page") {
|
|
30
|
+
throw new Error(`Unsupported assistant subcommand: ${normalizedSubcommand || "<empty>"}.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const targetFile = requireSinglePositionalTargetFile(args, { context: "assistant settings-page" });
|
|
34
|
+
rejectUnexpectedOptions(options, ["surface", "name", "link-placement", "link-component-token", "link-to", "force"], {
|
|
35
|
+
context: "assistant settings-page"
|
|
36
|
+
});
|
|
37
|
+
const forceOverwrite = Object.prototype.hasOwnProperty.call(options, "force")
|
|
38
|
+
? normalizeBoolean(options.force)
|
|
39
|
+
: false;
|
|
40
|
+
|
|
41
|
+
const appConfig = await loadAppConfig(appRoot);
|
|
42
|
+
const targetSurface = resolveSurfaceDefinition(appConfig, options?.surface, "surface");
|
|
43
|
+
const generationContext = await resolveAssistantPageGenerationContext({
|
|
44
|
+
appRoot,
|
|
45
|
+
targetFile,
|
|
46
|
+
options,
|
|
47
|
+
context: "assistant settings-page"
|
|
48
|
+
});
|
|
49
|
+
const pageTarget = generationContext.pageTarget;
|
|
50
|
+
const pageFilePath = pageTarget.targetFilePath.absolutePath;
|
|
51
|
+
const pageRelativePath = pageTarget.targetFilePath.relativePath;
|
|
52
|
+
const templateSource = await readAssistantPageTemplateSource("settings-page");
|
|
53
|
+
const desiredPageSource = renderAssistantPageSource(templateSource, targetSurface.id);
|
|
54
|
+
|
|
55
|
+
let existingPageSource = "";
|
|
56
|
+
let pageAlreadyExisted = true;
|
|
57
|
+
try {
|
|
58
|
+
existingPageSource = await readFile(pageFilePath, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
pageAlreadyExisted = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
requireEmptyPageSource(existingPageSource, pageRelativePath, {
|
|
64
|
+
context: "assistant settings-page",
|
|
65
|
+
forceOverwrite
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const touchedFiles = new Set();
|
|
69
|
+
if (!pageAlreadyExisted || forceOverwrite) {
|
|
70
|
+
if (dryRun !== true) {
|
|
71
|
+
await mkdir(path.dirname(pageFilePath), { recursive: true });
|
|
72
|
+
await writeFile(pageFilePath, desiredPageSource, "utf8");
|
|
73
|
+
}
|
|
74
|
+
touchedFiles.add(pageRelativePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const placementPath = resolvePathWithinApp(pageTarget.appRoot, PLACEMENT_FILE, {
|
|
78
|
+
context: "assistant settings-page"
|
|
79
|
+
});
|
|
80
|
+
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
81
|
+
const placementMarker =
|
|
82
|
+
`jskit:assistant.settings-page.link:${pageTarget.surfaceId}:${pageTarget.routeUrlSuffix}:${targetSurface.id}`;
|
|
83
|
+
const placementApplied = appendBlockIfMarkerMissing(
|
|
84
|
+
placementSource,
|
|
85
|
+
placementMarker,
|
|
86
|
+
renderAssistantPageLinkPlacementBlock({
|
|
87
|
+
marker: placementMarker,
|
|
88
|
+
pageTarget,
|
|
89
|
+
generationContext
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
if (placementApplied.changed) {
|
|
93
|
+
if (dryRun !== true) {
|
|
94
|
+
await writeFile(placementPath.absolutePath, placementApplied.content, "utf8");
|
|
95
|
+
}
|
|
96
|
+
touchedFiles.add(placementPath.relativePath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
101
|
+
summary: renderAssistantPageSummary(pageTarget, {
|
|
102
|
+
pageAlreadyExisted,
|
|
103
|
+
pageOverwritten: pageAlreadyExisted && forceOverwrite
|
|
104
|
+
})
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { runGeneratorSubcommand };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveRequiredAppRoot, toPosixPath } from "@jskit-ai/kernel/server/support";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
|
|
5
|
+
const PLACEMENT_FILE = "src/placement.js";
|
|
6
|
+
|
|
7
|
+
function requireSinglePositionalTargetFile(args = [], { context = "assistant" } = {}) {
|
|
8
|
+
const positionalArgs = Array.isArray(args) ? args.map((value) => normalizeText(value)).filter(Boolean) : [];
|
|
9
|
+
if (positionalArgs.length !== 1) {
|
|
10
|
+
throw new Error(`${context} requires exactly one <target-file> positional argument.`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return positionalArgs[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rejectUnexpectedOptions(options = {}, allowedOptionNames = [], { context = "assistant" } = {}) {
|
|
17
|
+
const allowedOptionNameSet = new Set(
|
|
18
|
+
(Array.isArray(allowedOptionNames) ? allowedOptionNames : [])
|
|
19
|
+
.map((optionName) => normalizeText(optionName))
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const unexpectedOptions = Object.keys(options || {})
|
|
24
|
+
.map((optionName) => normalizeText(optionName))
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.filter((optionName) => !allowedOptionNameSet.has(optionName))
|
|
27
|
+
.sort((left, right) => left.localeCompare(right));
|
|
28
|
+
|
|
29
|
+
if (unexpectedOptions.length < 1) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(
|
|
34
|
+
`${context} received unsupported option${unexpectedOptions.length > 1 ? "s" : ""}: ${unexpectedOptions.map((optionName) => `--${optionName}`).join(", ")}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolvePathWithinApp(appRoot, targetPath, { context = "assistant" } = {}) {
|
|
39
|
+
const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
|
|
40
|
+
context
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const normalizedTargetPath = normalizeText(targetPath);
|
|
44
|
+
if (!normalizedTargetPath) {
|
|
45
|
+
throw new Error(`${context} requires target path.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const absolutePath = path.resolve(resolvedAppRoot, normalizedTargetPath);
|
|
49
|
+
const relativePath = path.relative(resolvedAppRoot, absolutePath);
|
|
50
|
+
if (
|
|
51
|
+
!relativePath ||
|
|
52
|
+
relativePath === ".." ||
|
|
53
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
54
|
+
path.isAbsolute(relativePath)
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`${context} target path must stay within app root: ${normalizedTargetPath}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
absolutePath,
|
|
61
|
+
relativePath: toPosixPath(relativePath)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ensureTrailingNewline(value = "") {
|
|
66
|
+
const source = String(value || "");
|
|
67
|
+
return source.endsWith("\n") ? source : `${source}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appendBlockIfMarkerMissing(source = "", marker = "", block = "") {
|
|
71
|
+
const normalizedMarker = String(marker || "").trim();
|
|
72
|
+
const normalizedBlock = String(block || "").trim();
|
|
73
|
+
const sourceText = String(source || "");
|
|
74
|
+
if (!normalizedMarker || !normalizedBlock || sourceText.includes(normalizedMarker)) {
|
|
75
|
+
return {
|
|
76
|
+
changed: false,
|
|
77
|
+
content: sourceText
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
changed: true,
|
|
83
|
+
content: `${ensureTrailingNewline(sourceText)}${normalizedBlock}\n`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireEmptyPageSource(existingSource = "", targetRelativePath = "", { context = "assistant", forceOverwrite = false } = {}) {
|
|
88
|
+
const sourceText = String(existingSource || "");
|
|
89
|
+
if (!sourceText) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (forceOverwrite) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${context} will not overwrite existing page ${targetRelativePath}. Re-run with --force to overwrite it.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
PLACEMENT_FILE,
|
|
103
|
+
requireSinglePositionalTargetFile,
|
|
104
|
+
rejectUnexpectedOptions,
|
|
105
|
+
resolvePathWithinApp,
|
|
106
|
+
appendBlockIfMarkerMissing,
|
|
107
|
+
requireEmptyPageSource
|
|
108
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { loadAppConfigFromModuleUrl, resolveRequiredAppRoot } from "@jskit-ai/kernel/server/support";
|
|
4
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
5
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
|
|
7
|
+
|
|
8
|
+
function normalizeConfigScope(value = "") {
|
|
9
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
10
|
+
if (normalized === "global" || normalized === "workspace") {
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
throw new Error('assistant generator option "config-scope" must be "global" or "workspace".');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function loadAppConfig(appRoot = "") {
|
|
18
|
+
const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
|
|
19
|
+
context: "assistant generator"
|
|
20
|
+
});
|
|
21
|
+
const publicConfigUrl = pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href;
|
|
22
|
+
return loadAppConfigFromModuleUrl({
|
|
23
|
+
moduleUrl: publicConfigUrl
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveSurfaceDefinition(appConfig = {}, surfaceId = "", optionName = "surface") {
|
|
28
|
+
const normalizedSurfaceId = normalizeSurfaceId(surfaceId);
|
|
29
|
+
if (!normalizedSurfaceId) {
|
|
30
|
+
throw new Error(`assistant generator requires --${optionName}.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sourceDefinitions =
|
|
34
|
+
appConfig && typeof appConfig.surfaceDefinitions === "object" && !Array.isArray(appConfig.surfaceDefinitions)
|
|
35
|
+
? appConfig.surfaceDefinitions
|
|
36
|
+
: {};
|
|
37
|
+
const rawDefinition = sourceDefinitions[normalizedSurfaceId];
|
|
38
|
+
if (!rawDefinition || typeof rawDefinition !== "object" || Array.isArray(rawDefinition)) {
|
|
39
|
+
throw new Error(`assistant generator surface "${normalizedSurfaceId}" is not defined in config/public.js.`);
|
|
40
|
+
}
|
|
41
|
+
if (rawDefinition.enabled === false) {
|
|
42
|
+
throw new Error(`assistant generator surface "${normalizedSurfaceId}" is disabled in config/public.js.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
id: normalizedSurfaceId,
|
|
47
|
+
requiresWorkspace: rawDefinition.requiresWorkspace === true,
|
|
48
|
+
accessPolicyId: normalizeText(rawDefinition.accessPolicyId).toLowerCase()
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertAssistantSurfaceIsAvailable(appConfig = {}, surfaceId = "") {
|
|
53
|
+
const assistantSurfaces =
|
|
54
|
+
appConfig && typeof appConfig.assistantSurfaces === "object" && !Array.isArray(appConfig.assistantSurfaces)
|
|
55
|
+
? appConfig.assistantSurfaces
|
|
56
|
+
: {};
|
|
57
|
+
if (assistantSurfaces[surfaceId]) {
|
|
58
|
+
throw new Error(`assistant generator surface "${surfaceId}" already has an assistant configured in config/public.js.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveAiConfigPrefix(surfaceId = "", explicitPrefix = "") {
|
|
63
|
+
const normalizedExplicitPrefix = normalizeText(explicitPrefix);
|
|
64
|
+
if (normalizedExplicitPrefix) {
|
|
65
|
+
return normalizedExplicitPrefix;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const surfacePrefix = toSnakeCase(surfaceId).toUpperCase();
|
|
69
|
+
return surfacePrefix ? `${surfacePrefix}_ASSISTANT` : "ASSISTANT";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
normalizeConfigScope,
|
|
74
|
+
loadAppConfig,
|
|
75
|
+
resolveSurfaceDefinition,
|
|
76
|
+
assertAssistantSurfaceIsAvailable,
|
|
77
|
+
resolveAiConfigPrefix
|
|
78
|
+
};
|
|
@@ -9,7 +9,6 @@ async function withTempApp(run) {
|
|
|
9
9
|
const appRoot = await mkdtemp(path.join(tmpdir(), "assistant-generator-"));
|
|
10
10
|
try {
|
|
11
11
|
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
12
|
-
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
13
12
|
await writeFile(
|
|
14
13
|
path.join(appRoot, "package.json"),
|
|
15
14
|
`${JSON.stringify({ name: "assistant-generator-test-app", private: true, type: "module" }, null, 2)}\n`,
|
|
@@ -25,17 +24,6 @@ async function withTempApp(run) {
|
|
|
25
24
|
},
|
|
26
25
|
assistantSurfaces: {}
|
|
27
26
|
};
|
|
28
|
-
`,
|
|
29
|
-
"utf8"
|
|
30
|
-
);
|
|
31
|
-
await writeFile(
|
|
32
|
-
path.join(appRoot, "src", "components", "ShellLayout.vue"),
|
|
33
|
-
`<template>
|
|
34
|
-
<div>
|
|
35
|
-
<ShellOutlet host="shell-layout" position="primary-menu" />
|
|
36
|
-
<ShellOutlet host="shell-layout" position="top-right" default />
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
27
|
`,
|
|
40
28
|
"utf8"
|
|
41
29
|
);
|
|
@@ -46,29 +34,36 @@ async function withTempApp(run) {
|
|
|
46
34
|
}
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
test("buildTemplateContext derives
|
|
37
|
+
test("buildTemplateContext derives assistant setup placeholders from explicit setup surfaces", async () => {
|
|
50
38
|
await withTempApp(async (appRoot) => {
|
|
51
39
|
const context = await buildTemplateContext({
|
|
52
40
|
appRoot,
|
|
53
41
|
options: {
|
|
54
42
|
surface: "app",
|
|
55
43
|
"settings-surface": "console",
|
|
56
|
-
"config-scope": "global"
|
|
57
|
-
placement: "shell-layout:primary-menu",
|
|
58
|
-
"menu-label": "Copilot"
|
|
44
|
+
"config-scope": "global"
|
|
59
45
|
}
|
|
60
46
|
});
|
|
61
47
|
|
|
62
|
-
assert.equal(context.__ASSISTANT_SURFACE_ID__, "app");
|
|
63
48
|
assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_ID__, "console");
|
|
64
49
|
assert.equal(context.__ASSISTANT_CONFIG_SCOPE__, "global");
|
|
65
|
-
assert.equal(context.__ASSISTANT_SETTINGS_HOST__, "console-settings");
|
|
66
50
|
assert.equal(context.__ASSISTANT_AI_CONFIG_PREFIX__, "APP_ASSISTANT");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("buildTemplateContext honors explicit AI config prefix overrides", async () => {
|
|
55
|
+
await withTempApp(async (appRoot) => {
|
|
56
|
+
const context = await buildTemplateContext({
|
|
57
|
+
appRoot,
|
|
58
|
+
options: {
|
|
59
|
+
surface: "app",
|
|
60
|
+
"settings-surface": "console",
|
|
61
|
+
"config-scope": "global",
|
|
62
|
+
"ai-config-prefix": "CUSTOM_ASSISTANT"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.equal(context.__ASSISTANT_AI_CONFIG_PREFIX__, "CUSTOM_ASSISTANT");
|
|
72
67
|
});
|
|
73
68
|
});
|
|
74
69
|
|
|
@@ -81,8 +76,7 @@ test("buildTemplateContext rejects workspace config scope for a non-workspace as
|
|
|
81
76
|
options: {
|
|
82
77
|
surface: "app",
|
|
83
78
|
"settings-surface": "console",
|
|
84
|
-
"config-scope": "workspace"
|
|
85
|
-
placement: "shell-layout:primary-menu"
|
|
79
|
+
"config-scope": "workspace"
|
|
86
80
|
}
|
|
87
81
|
}),
|
|
88
82
|
/config-scope "workspace" requires surface "app" with requiresWorkspace=true/
|
|
@@ -117,8 +111,7 @@ test("buildTemplateContext rejects duplicate assistant surfaces already configur
|
|
|
117
111
|
options: {
|
|
118
112
|
surface: "app",
|
|
119
113
|
"settings-surface": "console",
|
|
120
|
-
"config-scope": "global"
|
|
121
|
-
placement: "shell-layout:primary-menu"
|
|
114
|
+
"config-scope": "global"
|
|
122
115
|
}
|
|
123
116
|
}),
|
|
124
117
|
/already has an assistant configured/
|