@jskit-ai/jskit-cli 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +3 -2
  2. package/src/server/cliRuntime/appState.js +226 -0
  3. package/src/server/cliRuntime/capabilitySupport.js +194 -0
  4. package/src/server/cliRuntime/descriptorValidation.js +150 -0
  5. package/src/server/cliRuntime/ioAndMigrations.js +381 -0
  6. package/src/server/cliRuntime/localPackageSupport.js +390 -0
  7. package/src/server/cliRuntime/mutationApplication.js +9 -0
  8. package/src/server/cliRuntime/mutationWhen.js +285 -0
  9. package/src/server/cliRuntime/mutations/fileMutations.js +247 -0
  10. package/src/server/cliRuntime/mutations/installMigrationMutation.js +213 -0
  11. package/src/server/cliRuntime/mutations/mutationPathUtils.js +12 -0
  12. package/src/server/cliRuntime/mutations/surfaceTargets.js +155 -0
  13. package/src/server/cliRuntime/mutations/templateContext.js +171 -0
  14. package/src/server/cliRuntime/mutations/textMutations.js +250 -0
  15. package/src/server/cliRuntime/packageInstallFlow.js +489 -0
  16. package/src/server/cliRuntime/packageIntrospection/exportEntries.js +259 -0
  17. package/src/server/cliRuntime/packageIntrospection/exportedSymbols.js +216 -0
  18. package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +98 -0
  19. package/src/server/cliRuntime/packageIntrospection/providerBindingIntrospection.js +377 -0
  20. package/src/server/cliRuntime/packageIntrospection.js +137 -0
  21. package/src/server/cliRuntime/packageOptions.js +299 -0
  22. package/src/server/cliRuntime/packageRegistries.js +343 -0
  23. package/src/server/cliRuntime/packageTemplateResolution.js +131 -0
  24. package/src/server/cliRuntime/viteProxy.js +356 -0
  25. package/src/server/commandHandlers/health.js +292 -0
  26. package/src/server/commandHandlers/list.js +292 -0
  27. package/src/server/commandHandlers/package.js +23 -0
  28. package/src/server/commandHandlers/packageCommands/add.js +282 -0
  29. package/src/server/commandHandlers/packageCommands/create.js +155 -0
  30. package/src/server/commandHandlers/packageCommands/generate.js +116 -0
  31. package/src/server/commandHandlers/packageCommands/migrations.js +155 -0
  32. package/src/server/commandHandlers/packageCommands/position.js +103 -0
  33. package/src/server/commandHandlers/packageCommands/remove.js +181 -0
  34. package/src/server/commandHandlers/packageCommands/update.js +40 -0
  35. package/src/server/commandHandlers/shared.js +314 -0
  36. package/src/server/commandHandlers/show/payloads.js +92 -0
  37. package/src/server/commandHandlers/show/renderBundleText.js +16 -0
  38. package/src/server/commandHandlers/show/renderHelpers.js +82 -0
  39. package/src/server/commandHandlers/show/renderPackageCapabilities.js +124 -0
  40. package/src/server/commandHandlers/show/renderPackageExports.js +203 -0
  41. package/src/server/commandHandlers/show/renderPackageText.js +332 -0
  42. package/src/server/commandHandlers/show.js +114 -0
  43. package/src/server/core/argParser.js +144 -0
  44. package/src/server/{runtimeDeps.js → core/buildCommandDeps.js} +2 -1
  45. package/src/server/core/commandCatalog.js +47 -0
  46. package/src/server/core/createCliRunner.js +150 -0
  47. package/src/server/core/createCommandHandlers.js +43 -0
  48. package/src/server/{runCli.js → core/dispatchCli.js} +14 -1
  49. package/src/server/core/usageHelp.js +344 -0
  50. package/src/server/index.js +1 -1
  51. package/src/server/{optionInterpolation.js → shared/optionInterpolation.js} +12 -1
  52. package/src/server/{pathResolution.js → shared/pathResolution.js} +1 -1
  53. package/src/server/argParser.js +0 -206
  54. package/src/server/cliRuntime.js +0 -4956
  55. package/src/server/commandHandlers.js +0 -2109
  56. /package/src/server/{cliError.js → shared/cliError.js} +0 -0
  57. /package/src/server/{collectionUtils.js → shared/collectionUtils.js} +0 -0
  58. /package/src/server/{outputFormatting.js → shared/outputFormatting.js} +0 -0
  59. /package/src/server/{packageIdHelpers.js → shared/packageIdHelpers.js} +0 -0
@@ -0,0 +1,171 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ writeFile
5
+ } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { createCliError } from "../../shared/cliError.js";
9
+ import {
10
+ ensureArray,
11
+ ensureObject
12
+ } from "../../shared/collectionUtils.js";
13
+ import { interpolateOptionValue } from "../../shared/optionInterpolation.js";
14
+ import {
15
+ fileExists,
16
+ resolveAppRelativePathWithinRoot
17
+ } from "../ioAndMigrations.js";
18
+
19
+ function interpolateFileMutationRecord(mutation, options, packageId) {
20
+ const mutationKey = String(
21
+ mutation?.id || mutation?.to || mutation?.toSurface || mutation?.from || "files"
22
+ ).trim();
23
+ const interpolate = (value, field) =>
24
+ interpolateOptionValue(String(value || ""), options, packageId, `${mutationKey}.${field}`);
25
+
26
+ return {
27
+ ...mutation,
28
+ from: interpolate(mutation.from, "from"),
29
+ to: interpolate(mutation.to, "to"),
30
+ toSurface: interpolate(mutation.toSurface, "toSurface"),
31
+ toSurfacePath: interpolate(mutation.toSurfacePath, "toSurfacePath"),
32
+ toDir: interpolate(mutation.toDir, "toDir"),
33
+ extension: interpolate(mutation.extension, "extension"),
34
+ id: interpolate(mutation.id, "id"),
35
+ category: interpolate(mutation.category, "category"),
36
+ reason: interpolate(mutation.reason, "reason"),
37
+ templateContext: mutation.templateContext
38
+ ? {
39
+ entrypoint: interpolate(mutation.templateContext.entrypoint, "templateContext.entrypoint"),
40
+ export: interpolate(mutation.templateContext.export, "templateContext.export")
41
+ }
42
+ : null
43
+ };
44
+ }
45
+
46
+ function applyTemplateContextReplacements(sourceContent, replacements) {
47
+ let output = String(sourceContent || "");
48
+ for (const [placeholder, value] of Object.entries(ensureObject(replacements))) {
49
+ const normalizedPlaceholder = String(placeholder || "");
50
+ if (!normalizedPlaceholder) {
51
+ continue;
52
+ }
53
+ output = output.split(normalizedPlaceholder).join(String(value == null ? "" : value));
54
+ }
55
+ return output;
56
+ }
57
+
58
+ async function copyTemplateFile(
59
+ sourcePath,
60
+ targetPath,
61
+ options,
62
+ packageId,
63
+ interpolationKey,
64
+ templateContextReplacements = null
65
+ ) {
66
+ const sourceContent = await readFile(sourcePath, "utf8");
67
+ let renderedContent = sourceContent.includes("${")
68
+ ? interpolateOptionValue(sourceContent, options, packageId, interpolationKey)
69
+ : sourceContent;
70
+ if (templateContextReplacements) {
71
+ renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
72
+ }
73
+
74
+ await mkdir(path.dirname(targetPath), { recursive: true });
75
+ await writeFile(targetPath, renderedContent, "utf8");
76
+ }
77
+
78
+ async function resolveTemplateContextReplacementsForMutation({
79
+ packageEntry,
80
+ mutation,
81
+ options,
82
+ appRoot,
83
+ sourcePath,
84
+ targetPaths,
85
+ mutationContext = "files mutation"
86
+ } = {}) {
87
+ const templateContext = ensureObject(mutation?.templateContext);
88
+ const hasTemplateContext = Object.keys(templateContext).length > 0;
89
+ const entrypoint = String(templateContext.entrypoint || "").trim();
90
+ const resolvedMutationContext = String(mutationContext || "files mutation").trim() || "files mutation";
91
+ if (!hasTemplateContext) {
92
+ return null;
93
+ }
94
+ if (!entrypoint) {
95
+ throw createCliError(
96
+ `Invalid ${resolvedMutationContext} in ${packageEntry.packageId}: templateContext.entrypoint is required when templateContext is set.`
97
+ );
98
+ }
99
+ const exportName = String(templateContext.export || "").trim() || "buildTemplateContext";
100
+ const resolvedEntrypointPath = resolveAppRelativePathWithinRoot(
101
+ packageEntry.rootDir,
102
+ entrypoint,
103
+ `${packageEntry.packageId} ${resolvedMutationContext} templateContext.entrypoint`
104
+ );
105
+ const absoluteEntrypointPath = resolvedEntrypointPath.absolutePath;
106
+ if (!(await fileExists(absoluteEntrypointPath))) {
107
+ throw createCliError(
108
+ `Invalid ${resolvedMutationContext} in ${packageEntry.packageId}: templateContext.entrypoint not found at ${entrypoint}.`
109
+ );
110
+ }
111
+
112
+ let moduleNamespace = null;
113
+ try {
114
+ moduleNamespace = await import(`${pathToFileURL(absoluteEntrypointPath).href}?t=${Date.now()}_${Math.random()}`);
115
+ } catch (error) {
116
+ throw createCliError(
117
+ `Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
118
+ );
119
+ }
120
+
121
+ const resolver = moduleNamespace?.[exportName];
122
+ if (typeof resolver !== "function") {
123
+ throw createCliError(
124
+ `Invalid ${resolvedMutationContext} in ${packageEntry.packageId}: templateContext export "${exportName}" is not a function.`
125
+ );
126
+ }
127
+
128
+ let replacements = null;
129
+ try {
130
+ replacements = await resolver({
131
+ packageId: packageEntry.packageId,
132
+ packageRoot: packageEntry.rootDir,
133
+ appRoot,
134
+ options: Object.freeze({ ...ensureObject(options) }),
135
+ mutation: Object.freeze({ ...ensureObject(mutation) }),
136
+ sourcePath,
137
+ targetPaths: Object.freeze([...ensureArray(targetPaths)])
138
+ });
139
+ } catch (error) {
140
+ throw createCliError(
141
+ `templateContext export "${exportName}" failed for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
142
+ );
143
+ }
144
+
145
+ if (replacements == null) {
146
+ return null;
147
+ }
148
+ if (!replacements || typeof replacements !== "object" || Array.isArray(replacements)) {
149
+ throw createCliError(
150
+ `Invalid ${resolvedMutationContext} in ${packageEntry.packageId}: templateContext export "${exportName}" must return an object map of placeholder replacements.`
151
+ );
152
+ }
153
+
154
+ const normalizedReplacements = {};
155
+ for (const [placeholder, value] of Object.entries(replacements)) {
156
+ const normalizedPlaceholder = String(placeholder || "").trim();
157
+ if (!normalizedPlaceholder) {
158
+ continue;
159
+ }
160
+ normalizedReplacements[normalizedPlaceholder] = String(value == null ? "" : value);
161
+ }
162
+
163
+ return Object.freeze(normalizedReplacements);
164
+ }
165
+
166
+ export {
167
+ applyTemplateContextReplacements,
168
+ copyTemplateFile,
169
+ interpolateFileMutationRecord,
170
+ resolveTemplateContextReplacementsForMutation
171
+ };
@@ -0,0 +1,250 @@
1
+ import {
2
+ mkdir,
3
+ writeFile
4
+ } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { createCliError } from "../../shared/cliError.js";
7
+ import {
8
+ ensureArray,
9
+ ensureObject
10
+ } from "../../shared/collectionUtils.js";
11
+ import {
12
+ appendTextSnippet,
13
+ interpolateOptionValue,
14
+ normalizeSkipChecks
15
+ } from "../../shared/optionInterpolation.js";
16
+ import {
17
+ normalizeFileMutationRecord,
18
+ normalizeMutationWhen,
19
+ shouldApplyMutationWhen
20
+ } from "../mutationWhen.js";
21
+ import {
22
+ loadMutationWhenConfigContext,
23
+ normalizeRelativePath,
24
+ readFileBufferIfExists
25
+ } from "../ioAndMigrations.js";
26
+ import { upsertEnvValue } from "../appState.js";
27
+ import {
28
+ applyTemplateContextReplacements,
29
+ resolveTemplateContextReplacementsForMutation
30
+ } from "./templateContext.js";
31
+ import { normalizeMutationRelativeFilePath } from "./mutationPathUtils.js";
32
+
33
+ const SETTINGS_FIELDS_CONTRACT_TARGETS = Object.freeze({
34
+ "packages/main/src/shared/resources/consoleSettingsFields.js": Object.freeze({
35
+ contractId: "users.settings-fields.console.v1",
36
+ marker: "@jskit-contract users.settings-fields.console.v1",
37
+ requiredSnippets: Object.freeze([
38
+ "defineField",
39
+ "resetConsoleSettingsFields"
40
+ ])
41
+ }),
42
+ "packages/main/src/shared/resources/workspaceSettingsFields.js": Object.freeze({
43
+ contractId: "users.settings-fields.workspace.v1",
44
+ marker: "@jskit-contract users.settings-fields.workspace.v1",
45
+ requiredSnippets: Object.freeze([
46
+ "defineField",
47
+ "resetWorkspaceSettingsFields"
48
+ ])
49
+ })
50
+ });
51
+
52
+ function resolveSettingsFieldsContractTarget(relativeFile = "") {
53
+ const normalizedRelativeFile = normalizeMutationRelativeFilePath(relativeFile);
54
+ if (!normalizedRelativeFile) {
55
+ return null;
56
+ }
57
+ const target = SETTINGS_FIELDS_CONTRACT_TARGETS[normalizedRelativeFile];
58
+ if (!target) {
59
+ return null;
60
+ }
61
+ return {
62
+ normalizedRelativeFile,
63
+ target
64
+ };
65
+ }
66
+
67
+ async function validateSettingsFieldsContractMutationTarget({
68
+ appRoot,
69
+ relativeFile,
70
+ packageId
71
+ } = {}) {
72
+ const contractTarget = resolveSettingsFieldsContractTarget(relativeFile);
73
+ if (!contractTarget) {
74
+ return;
75
+ }
76
+
77
+ const { normalizedRelativeFile, target } = contractTarget;
78
+ const absoluteFile = path.join(appRoot, normalizedRelativeFile);
79
+ const existing = await readFileBufferIfExists(absoluteFile);
80
+ if (!existing.exists) {
81
+ throw createCliError(
82
+ `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing. ` +
83
+ `Install @jskit-ai/users-core to scaffold ${target.contractId}.`
84
+ );
85
+ }
86
+
87
+ const source = existing.buffer.toString("utf8");
88
+ if (!source.includes(target.marker)) {
89
+ throw createCliError(
90
+ `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} is missing contract marker "${target.marker}".`
91
+ );
92
+ }
93
+ for (const snippet of target.requiredSnippets) {
94
+ if (source.includes(snippet)) {
95
+ continue;
96
+ }
97
+ throw createCliError(
98
+ `Invalid append-text mutation in ${packageId}: ${normalizedRelativeFile} must include "${snippet}" for ${target.contractId}.`
99
+ );
100
+ }
101
+ }
102
+
103
+ async function applyTextMutations(packageEntry, appRoot, textMutations, options, managedText, touchedFiles) {
104
+ for (const mutation of textMutations) {
105
+ const when = normalizeMutationWhen(mutation?.when);
106
+ const configContext = when?.config ? await loadMutationWhenConfigContext(appRoot) : {};
107
+ if (
108
+ !shouldApplyMutationWhen(when, {
109
+ options,
110
+ configContext,
111
+ packageId: packageEntry.packageId,
112
+ mutationContext: "text mutation"
113
+ })
114
+ ) {
115
+ continue;
116
+ }
117
+
118
+ const operation = String(mutation?.op || "").trim();
119
+ if (operation === "upsert-env") {
120
+ const relativeFile = String(mutation?.file || "").trim();
121
+ const key = String(mutation?.key || "").trim();
122
+ if (!relativeFile || !key) {
123
+ throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: "file" and "key" are required.`);
124
+ }
125
+
126
+ const absoluteFile = path.join(appRoot, relativeFile);
127
+ const previous = await readFileBufferIfExists(absoluteFile);
128
+ const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
129
+ const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, key);
130
+ const upserted = upsertEnvValue(previousContent, key, resolvedValue);
131
+
132
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
133
+ await writeFile(absoluteFile, upserted.content, "utf8");
134
+
135
+ const recordKey = `${relativeFile}::${String(mutation?.id || key)}`;
136
+ managedText[recordKey] = {
137
+ file: relativeFile,
138
+ op: "upsert-env",
139
+ key,
140
+ value: resolvedValue,
141
+ hadPrevious: upserted.hadPrevious,
142
+ previousValue: upserted.previousValue,
143
+ reason: String(mutation?.reason || ""),
144
+ category: String(mutation?.category || ""),
145
+ id: String(mutation?.id || "")
146
+ };
147
+ touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
148
+ continue;
149
+ }
150
+
151
+ if (operation === "append-text") {
152
+ const relativeFile = String(mutation?.file || "").trim();
153
+ const snippet = String(mutation?.value || "");
154
+ const position = String(mutation?.position || "bottom").trim().toLowerCase();
155
+ if (!relativeFile) {
156
+ throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "file" is required.`);
157
+ }
158
+ if (position !== "top" && position !== "bottom") {
159
+ throw createCliError(`Invalid append-text mutation in ${packageEntry.packageId}: "position" must be "top" or "bottom".`);
160
+ }
161
+ await validateSettingsFieldsContractMutationTarget({
162
+ appRoot,
163
+ relativeFile,
164
+ packageId: packageEntry.packageId
165
+ });
166
+
167
+ const absoluteFile = path.join(appRoot, relativeFile);
168
+ const previous = await readFileBufferIfExists(absoluteFile);
169
+ const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
170
+ const mutationId = String(mutation?.id || "").trim() || "append-text";
171
+ const resolvedSnippet = interpolateOptionValue(snippet, options, packageEntry.packageId, mutationId);
172
+ const templateContextReplacements = await resolveTemplateContextReplacementsForMutation({
173
+ packageEntry,
174
+ mutation,
175
+ options,
176
+ appRoot,
177
+ sourcePath: absoluteFile,
178
+ targetPaths: [absoluteFile],
179
+ mutationContext: "text mutation"
180
+ });
181
+ const renderedSnippet = templateContextReplacements
182
+ ? applyTemplateContextReplacements(resolvedSnippet, templateContextReplacements)
183
+ : resolvedSnippet;
184
+ const skipChecks = normalizeSkipChecks(mutation?.skipIfContains)
185
+ .map((entry) => interpolateOptionValue(entry, options, packageEntry.packageId, `${mutationId}.skipIfContains`))
186
+ .map((entry) => {
187
+ if (!templateContextReplacements) {
188
+ return entry;
189
+ }
190
+ return applyTemplateContextReplacements(entry, templateContextReplacements);
191
+ })
192
+ .filter((entry) => String(entry || "").trim().length > 0);
193
+
194
+ const shouldSkip = skipChecks.some((pattern) => previousContent.includes(String(pattern)));
195
+ if (shouldSkip) {
196
+ continue;
197
+ }
198
+
199
+ const appended = appendTextSnippet(previousContent, renderedSnippet, position);
200
+ if (!appended.changed) {
201
+ continue;
202
+ }
203
+
204
+ await mkdir(path.dirname(absoluteFile), { recursive: true });
205
+ await writeFile(absoluteFile, appended.content, "utf8");
206
+
207
+ const recordKey = `${relativeFile}::${mutationId}`;
208
+ managedText[recordKey] = {
209
+ file: relativeFile,
210
+ op: "append-text",
211
+ value: renderedSnippet,
212
+ position,
213
+ reason: String(mutation?.reason || ""),
214
+ category: String(mutation?.category || ""),
215
+ id: String(mutation?.id || "")
216
+ };
217
+ touchedFiles.add(normalizeRelativePath(appRoot, absoluteFile));
218
+ continue;
219
+ }
220
+
221
+ throw createCliError(`Unsupported text mutation op "${operation}" in ${packageEntry.packageId}.`);
222
+ }
223
+ }
224
+
225
+ function isPositioningTextMutation(value = {}) {
226
+ const mutation = ensureObject(value);
227
+ const operation = String(mutation.op || "").trim();
228
+ if (operation !== "append-text") {
229
+ return false;
230
+ }
231
+ return normalizeMutationRelativeFilePath(mutation.file) === "src/placement.js";
232
+ }
233
+
234
+ function resolvePositioningMutations(descriptorMutations = {}) {
235
+ const mutations = ensureObject(descriptorMutations);
236
+ const files = ensureArray(mutations.files).filter((mutationValue) => {
237
+ const normalized = normalizeFileMutationRecord(mutationValue);
238
+ return Boolean(normalized.toSurface);
239
+ });
240
+ const text = ensureArray(mutations.text).filter((mutationValue) => isPositioningTextMutation(mutationValue));
241
+ return {
242
+ files,
243
+ text
244
+ };
245
+ }
246
+
247
+ export {
248
+ applyTextMutations,
249
+ resolvePositioningMutations
250
+ };