@jskit-ai/jskit-cli 0.2.41 → 0.2.43
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/package.json +4 -3
- package/src/server/cliRuntime/completion.js +1177 -0
- package/src/server/cliRuntime/descriptorValidation.js +18 -3
- package/src/server/cliRuntime/ioAndMigrations.js +2 -2
- package/src/server/cliRuntime/mutationApplication.js +1 -1
- package/src/server/cliRuntime/mutationWhen.js +2 -0
- package/src/server/cliRuntime/mutations/fileMutations.js +188 -143
- package/src/server/cliRuntime/mutations/installMigrationMutation.js +11 -38
- package/src/server/cliRuntime/mutations/templateContext.js +8 -14
- package/src/server/cliRuntime/mutations/textMutations.js +11 -6
- package/src/server/cliRuntime/packageInstallFlow.js +36 -21
- package/src/server/cliRuntime/packageIntrospection/placementNormalization.js +13 -22
- package/src/server/cliRuntime/packageOptions.js +149 -3
- package/src/server/cliRuntime/packageRegistries.js +3 -2
- package/src/server/commandHandlers/completion.js +129 -0
- package/src/server/commandHandlers/list.js +4 -6
- package/src/server/commandHandlers/packageCommands/add.js +31 -11
- package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +10 -2
- package/src/server/commandHandlers/packageCommands/generate.js +29 -31
- package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +123 -164
- package/src/server/commandHandlers/shared.js +23 -3
- package/src/server/commandHandlers/show/renderPackageText.js +3 -3
- package/src/server/core/argParser.js +12 -2
- package/src/server/core/commandCatalog.js +36 -13
- package/src/server/core/createCommandHandlers.js +3 -0
- package/src/server/shared/optionInterpolation.js +93 -0
|
@@ -24,12 +24,27 @@ function normalizePackageKind(rawValue, descriptorPath) {
|
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function
|
|
27
|
+
function validateFileMutationShape(descriptor, descriptorPath) {
|
|
28
28
|
const packageId = String(ensureObject(descriptor).packageId || "").trim() || "unknown-package";
|
|
29
29
|
const mutations = ensureObject(ensureObject(descriptor).mutations);
|
|
30
30
|
const files = ensureArray(mutations.files);
|
|
31
31
|
for (const fileMutation of files) {
|
|
32
32
|
const normalized = normalizeFileMutationRecord(fileMutation);
|
|
33
|
+
if (normalized.ownership !== "package" && normalized.ownership !== "app") {
|
|
34
|
+
throw createCliError(
|
|
35
|
+
`Invalid package descriptor at ${descriptorPath}: files mutation in ${packageId} has unsupported ownership "${normalized.ownership}". Expected "package" or "app".`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (normalized.expectedExistingFrom && normalized.op !== "copy-file") {
|
|
39
|
+
throw createCliError(
|
|
40
|
+
`Invalid package descriptor at ${descriptorPath}: files mutation in ${packageId} can only use "expectedExistingFrom" with copy-file.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (normalized.expectedExistingFrom && normalized.ownership !== "app") {
|
|
44
|
+
throw createCliError(
|
|
45
|
+
`Invalid package descriptor at ${descriptorPath}: files mutation in ${packageId} can only use "expectedExistingFrom" when ownership is "app".`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
33
48
|
if (normalized.op !== "install-migration") {
|
|
34
49
|
continue;
|
|
35
50
|
}
|
|
@@ -69,7 +84,7 @@ function validatePackageDescriptorShape(descriptor, descriptorPath) {
|
|
|
69
84
|
);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
|
|
87
|
+
validateFileMutationShape(normalized, descriptorPath);
|
|
73
88
|
|
|
74
89
|
return {
|
|
75
90
|
...normalized,
|
|
@@ -99,7 +114,7 @@ function validateAppLocalPackageDescriptorShape(descriptor, descriptorPath, { ex
|
|
|
99
114
|
throw createCliError(`Invalid app-local package descriptor at ${descriptorPath}: missing version.`);
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
validateFileMutationShape(normalized, descriptorPath);
|
|
103
118
|
|
|
104
119
|
return {
|
|
105
120
|
...normalized,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
writeFile
|
|
9
9
|
} from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { importFreshModuleFromAbsolutePath } from "@jskit-ai/kernel/server/support";
|
|
12
12
|
import { createCliError } from "../shared/cliError.js";
|
|
13
13
|
import {
|
|
14
14
|
ensureArray,
|
|
@@ -325,7 +325,7 @@ async function loadAppConfigModuleConfig(appRoot, relativePath) {
|
|
|
325
325
|
|
|
326
326
|
let moduleNamespace = null;
|
|
327
327
|
try {
|
|
328
|
-
moduleNamespace = await
|
|
328
|
+
moduleNamespace = await importFreshModuleFromAbsolutePath(absolutePath);
|
|
329
329
|
} catch (error) {
|
|
330
330
|
throw createCliError(
|
|
331
331
|
`Unable to load ${relativePath}: ${String(error?.message || error || "unknown error")}`
|
|
@@ -41,6 +41,8 @@ function normalizeFileMutationRecord(value) {
|
|
|
41
41
|
toSurfaceRoot: record.toSurfaceRoot === true,
|
|
42
42
|
toDir: String(record.toDir || "").trim(),
|
|
43
43
|
extension: normalizeMutationExtension(record.extension),
|
|
44
|
+
ownership: String(record.ownership || "").trim().toLowerCase() || "package",
|
|
45
|
+
expectedExistingFrom: String(record.expectedExistingFrom || "").trim(),
|
|
44
46
|
preserveOnRemove: record.preserveOnRemove === true,
|
|
45
47
|
id: String(record.id || "").trim(),
|
|
46
48
|
category: String(record.category || "").trim(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
mkdir,
|
|
3
|
+
writeFile
|
|
3
4
|
} from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { createCliError } from "../../shared/cliError.js";
|
|
@@ -20,151 +21,32 @@ import {
|
|
|
20
21
|
resolveAppRelativePathWithinRoot
|
|
21
22
|
} from "../ioAndMigrations.js";
|
|
22
23
|
import {
|
|
23
|
-
copyTemplateFile,
|
|
24
24
|
interpolateFileMutationRecord,
|
|
25
|
+
renderTemplateFile,
|
|
25
26
|
resolveTemplateContextReplacementsForMutation
|
|
26
27
|
} from "./templateContext.js";
|
|
27
28
|
import { resolveSurfaceTargetPathsForMutation } from "./surfaceTargets.js";
|
|
28
29
|
import { applyInstallMigrationMutation } from "./installMigrationMutation.js";
|
|
29
30
|
|
|
30
|
-
async function
|
|
31
|
+
async function prepareFileMutations(
|
|
31
32
|
packageEntry,
|
|
32
33
|
options,
|
|
33
34
|
appRoot,
|
|
34
35
|
fileMutations,
|
|
35
|
-
|
|
36
|
-
managedMigrations,
|
|
37
|
-
touchedFiles,
|
|
38
|
-
warnings = [],
|
|
39
|
-
precomputedTemplateContextByMutationIndex = null
|
|
36
|
+
existingManagedFiles = []
|
|
40
37
|
) {
|
|
41
38
|
const mutationList = ensureArray(fileMutations);
|
|
42
|
-
const
|
|
43
|
-
for (const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
managedMigrationById.set(migrationId, managedMigration);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const [mutationIndex, mutationValue] of mutationList.entries()) {
|
|
53
|
-
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
54
|
-
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
55
|
-
const configContext = requiresConfigContext ? await loadMutationWhenConfigContext(appRoot) : {};
|
|
56
|
-
if (
|
|
57
|
-
!shouldApplyMutationWhen(normalizedMutation.when, {
|
|
58
|
-
options,
|
|
59
|
-
configContext,
|
|
60
|
-
packageId: packageEntry.packageId,
|
|
61
|
-
mutationContext: "files mutation"
|
|
62
|
-
})
|
|
63
|
-
) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
|
|
68
|
-
const operation = mutation.op || "copy-file";
|
|
69
|
-
|
|
70
|
-
if (operation === "install-migration") {
|
|
71
|
-
await applyInstallMigrationMutation({
|
|
72
|
-
packageEntry,
|
|
73
|
-
mutation,
|
|
74
|
-
rawMutation: mutationValue,
|
|
75
|
-
mutationIndex,
|
|
76
|
-
options,
|
|
77
|
-
appRoot,
|
|
78
|
-
managedMigrations,
|
|
79
|
-
managedMigrationById,
|
|
80
|
-
touchedFiles,
|
|
81
|
-
warnings,
|
|
82
|
-
precomputedTemplateContextByMutationIndex
|
|
83
|
-
});
|
|
39
|
+
const existingManagedFilesByPath = new Map();
|
|
40
|
+
for (const managedFileValue of ensureArray(existingManagedFiles)) {
|
|
41
|
+
const managedFile = ensureObject(managedFileValue);
|
|
42
|
+
const managedPath = String(managedFile.path || "").trim();
|
|
43
|
+
if (!managedPath) {
|
|
84
44
|
continue;
|
|
85
45
|
}
|
|
86
|
-
|
|
87
|
-
if (operation !== "copy-file") {
|
|
88
|
-
throw createCliError(`Unsupported files mutation op \"${operation}\" in ${packageEntry.packageId}.`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const from = mutation.from;
|
|
92
|
-
const to = mutation.to;
|
|
93
|
-
const toSurface = mutation.toSurface;
|
|
94
|
-
if (to && toSurface) {
|
|
95
|
-
throw createCliError(
|
|
96
|
-
`Invalid files mutation in ${packageEntry.packageId}: "to" and "toSurface" cannot both be set.`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (!from || (!to && !toSurface)) {
|
|
100
|
-
throw createCliError(
|
|
101
|
-
`Invalid files mutation in ${packageEntry.packageId}: "from" plus one destination ("to" or "toSurface") are required.`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
106
|
-
if (!(await fileExists(sourcePath))) {
|
|
107
|
-
throw createCliError(`Missing template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const targetPaths = toSurface
|
|
111
|
-
? resolveSurfaceTargetPathsForMutation({
|
|
112
|
-
appRoot,
|
|
113
|
-
packageId: packageEntry.packageId,
|
|
114
|
-
mutation,
|
|
115
|
-
configContext
|
|
116
|
-
})
|
|
117
|
-
: [resolveAppRelativePathWithinRoot(appRoot, to, `${packageEntry.packageId} files mutation.to`).absolutePath];
|
|
118
|
-
const hasPrecomputedTemplateContext =
|
|
119
|
-
precomputedTemplateContextByMutationIndex instanceof Map &&
|
|
120
|
-
precomputedTemplateContextByMutationIndex.has(mutationIndex);
|
|
121
|
-
const templateContextReplacements = hasPrecomputedTemplateContext
|
|
122
|
-
? precomputedTemplateContextByMutationIndex.get(mutationIndex)
|
|
123
|
-
: await resolveTemplateContextReplacementsForMutation({
|
|
124
|
-
packageEntry,
|
|
125
|
-
mutation,
|
|
126
|
-
options,
|
|
127
|
-
appRoot,
|
|
128
|
-
sourcePath,
|
|
129
|
-
targetPaths
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
for (const targetPath of targetPaths) {
|
|
133
|
-
const previous = await readFileBufferIfExists(targetPath);
|
|
134
|
-
await copyTemplateFile(
|
|
135
|
-
sourcePath,
|
|
136
|
-
targetPath,
|
|
137
|
-
options,
|
|
138
|
-
packageEntry.packageId,
|
|
139
|
-
`${mutation.id || to || from}.source`,
|
|
140
|
-
templateContextReplacements
|
|
141
|
-
);
|
|
142
|
-
const nextBuffer = await readFile(targetPath);
|
|
143
|
-
|
|
144
|
-
managedFiles.push({
|
|
145
|
-
path: normalizeRelativePath(appRoot, targetPath),
|
|
146
|
-
hash: hashBuffer(nextBuffer),
|
|
147
|
-
hadPrevious: previous.exists,
|
|
148
|
-
previousContentBase64: previous.exists ? previous.buffer.toString("base64") : "",
|
|
149
|
-
preserveOnRemove: mutation.preserveOnRemove,
|
|
150
|
-
reason: mutation.reason,
|
|
151
|
-
category: mutation.category,
|
|
152
|
-
id: mutation.id
|
|
153
|
-
});
|
|
154
|
-
touchedFiles.add(normalizeRelativePath(appRoot, targetPath));
|
|
155
|
-
}
|
|
46
|
+
existingManagedFilesByPath.set(managedPath, managedFile);
|
|
156
47
|
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function preflightFileMutationTemplateContexts(
|
|
160
|
-
packageEntry,
|
|
161
|
-
options,
|
|
162
|
-
appRoot,
|
|
163
|
-
fileMutations
|
|
164
|
-
) {
|
|
165
|
-
const mutationList = ensureArray(fileMutations);
|
|
166
|
-
const replacementsByMutationIndex = new Map();
|
|
167
48
|
|
|
49
|
+
const preparedMutations = [];
|
|
168
50
|
for (const [mutationIndex, mutationValue] of mutationList.entries()) {
|
|
169
51
|
const normalizedMutation = normalizeFileMutationRecord(mutationValue);
|
|
170
52
|
const requiresConfigContext = Boolean(normalizedMutation.when?.config || normalizedMutation.toSurface);
|
|
@@ -181,23 +63,16 @@ async function preflightFileMutationTemplateContexts(
|
|
|
181
63
|
}
|
|
182
64
|
|
|
183
65
|
const mutation = interpolateFileMutationRecord(normalizedMutation, options, packageEntry.packageId);
|
|
184
|
-
const templateContext = ensureObject(mutation.templateContext);
|
|
185
|
-
if (Object.keys(templateContext).length < 1) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
66
|
const operation = mutation.op || "copy-file";
|
|
190
67
|
if (operation !== "copy-file" && operation !== "install-migration") {
|
|
191
|
-
|
|
68
|
+
throw createCliError(`Unsupported files mutation op "${operation}" in ${packageEntry.packageId}.`);
|
|
192
69
|
}
|
|
193
70
|
|
|
194
71
|
const from = mutation.from;
|
|
195
72
|
const to = mutation.to;
|
|
196
73
|
const toSurface = mutation.toSurface;
|
|
197
74
|
if (!from) {
|
|
198
|
-
throw createCliError(
|
|
199
|
-
`Invalid files mutation in ${packageEntry.packageId}: "from" is required.`
|
|
200
|
-
);
|
|
75
|
+
throw createCliError(`Invalid files mutation in ${packageEntry.packageId}: "from" is required.`);
|
|
201
76
|
}
|
|
202
77
|
if (operation === "copy-file") {
|
|
203
78
|
if (to && toSurface) {
|
|
@@ -227,7 +102,7 @@ async function preflightFileMutationTemplateContexts(
|
|
|
227
102
|
})
|
|
228
103
|
: [resolveAppRelativePathWithinRoot(appRoot, to, `${packageEntry.packageId} files mutation.to`).absolutePath]
|
|
229
104
|
: [path.join(appRoot, mutation.toDir || "migrations")];
|
|
230
|
-
const
|
|
105
|
+
const templateContextReplacements = await resolveTemplateContextReplacementsForMutation({
|
|
231
106
|
packageEntry,
|
|
232
107
|
mutation,
|
|
233
108
|
options,
|
|
@@ -236,13 +111,183 @@ async function preflightFileMutationTemplateContexts(
|
|
|
236
111
|
targetPaths,
|
|
237
112
|
mutationContext: "files mutation"
|
|
238
113
|
});
|
|
239
|
-
|
|
114
|
+
const interpolationKey = `${mutation.id || to || from}.source`;
|
|
115
|
+
const renderedSourceContent = await renderTemplateFile(
|
|
116
|
+
sourcePath,
|
|
117
|
+
options,
|
|
118
|
+
packageEntry.packageId,
|
|
119
|
+
interpolationKey,
|
|
120
|
+
templateContextReplacements
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (operation === "copy-file" && mutation.ownership === "app") {
|
|
124
|
+
const renderedSourceBuffer = Buffer.from(renderedSourceContent, "utf8");
|
|
125
|
+
const renderedSourceHash = hashBuffer(renderedSourceBuffer);
|
|
126
|
+
let expectedExistingHash = "";
|
|
127
|
+
if (mutation.expectedExistingFrom) {
|
|
128
|
+
const expectedExistingPath = path.join(packageEntry.rootDir, mutation.expectedExistingFrom);
|
|
129
|
+
if (!(await fileExists(expectedExistingPath))) {
|
|
130
|
+
throw createCliError(
|
|
131
|
+
`Missing expected existing source ${expectedExistingPath} for ${packageEntry.packageId}.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const expectedExistingContent = await renderTemplateFile(
|
|
135
|
+
expectedExistingPath,
|
|
136
|
+
options,
|
|
137
|
+
packageEntry.packageId,
|
|
138
|
+
`${mutation.id || to || from}.expectedExisting`,
|
|
139
|
+
templateContextReplacements
|
|
140
|
+
);
|
|
141
|
+
expectedExistingHash = hashBuffer(Buffer.from(expectedExistingContent, "utf8"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const targetPath of targetPaths) {
|
|
145
|
+
const relativeTargetPath = normalizeRelativePath(appRoot, targetPath);
|
|
146
|
+
if (existingManagedFilesByPath.has(relativeTargetPath)) {
|
|
147
|
+
const existing = await readFileBufferIfExists(targetPath);
|
|
148
|
+
if (!existing.exists) {
|
|
149
|
+
throw createCliError(
|
|
150
|
+
`${packageEntry.packageId}: app-owned file ${relativeTargetPath} is managed in lock but missing on disk. Restore it before updating, or remove and re-add the package intentionally.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const existing = await readFileBufferIfExists(targetPath);
|
|
157
|
+
if (!existing.exists) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const existingHash = hashBuffer(existing.buffer);
|
|
162
|
+
if (existingHash === renderedSourceHash) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (expectedExistingHash && existingHash === expectedExistingHash) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const expectedSourceLabel = mutation.expectedExistingFrom
|
|
170
|
+
? ` or match ${mutation.expectedExistingFrom}`
|
|
171
|
+
: "";
|
|
172
|
+
throw createCliError(
|
|
173
|
+
`${packageEntry.packageId}: app-owned file ${relativeTargetPath} already exists and cannot be claimed. It must already match the rendered scaffold${expectedSourceLabel}.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
preparedMutations.push({
|
|
179
|
+
mutationIndex,
|
|
180
|
+
mutation,
|
|
181
|
+
operation,
|
|
182
|
+
sourcePath,
|
|
183
|
+
targetPaths,
|
|
184
|
+
renderedSourceContent
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return preparedMutations;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function applyFileMutations(
|
|
192
|
+
packageEntry,
|
|
193
|
+
appRoot,
|
|
194
|
+
preparedMutations,
|
|
195
|
+
managedFiles,
|
|
196
|
+
managedMigrations,
|
|
197
|
+
touchedFiles,
|
|
198
|
+
warnings = [],
|
|
199
|
+
existingManagedFiles = []
|
|
200
|
+
) {
|
|
201
|
+
const existingManagedFilesByPath = new Map();
|
|
202
|
+
for (const managedFileValue of ensureArray(existingManagedFiles)) {
|
|
203
|
+
const managedFile = ensureObject(managedFileValue);
|
|
204
|
+
const managedPath = String(managedFile.path || "").trim();
|
|
205
|
+
if (!managedPath) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
existingManagedFilesByPath.set(managedPath, managedFile);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const managedMigrationById = new Map();
|
|
212
|
+
for (const managedMigrationValue of ensureArray(managedMigrations)) {
|
|
213
|
+
const managedMigration = ensureObject(managedMigrationValue);
|
|
214
|
+
const migrationId = String(managedMigration.id || "").trim();
|
|
215
|
+
if (!migrationId) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
managedMigrationById.set(migrationId, managedMigration);
|
|
240
219
|
}
|
|
241
220
|
|
|
242
|
-
|
|
221
|
+
for (const preparedMutation of ensureArray(preparedMutations)) {
|
|
222
|
+
const mutation = ensureObject(preparedMutation.mutation);
|
|
223
|
+
const operation = String(preparedMutation.operation || "").trim() || "copy-file";
|
|
224
|
+
if (operation === "install-migration") {
|
|
225
|
+
await applyInstallMigrationMutation({
|
|
226
|
+
packageEntry,
|
|
227
|
+
preparedMutation,
|
|
228
|
+
appRoot,
|
|
229
|
+
managedMigrations,
|
|
230
|
+
managedMigrationById,
|
|
231
|
+
touchedFiles,
|
|
232
|
+
warnings
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const renderedSourceContent = String(preparedMutation.renderedSourceContent || "");
|
|
238
|
+
const renderedSourceBuffer = Buffer.from(renderedSourceContent, "utf8");
|
|
239
|
+
const renderedSourceHash = hashBuffer(renderedSourceBuffer);
|
|
240
|
+
|
|
241
|
+
for (const targetPath of ensureArray(preparedMutation.targetPaths)) {
|
|
242
|
+
const relativeTargetPath = normalizeRelativePath(appRoot, targetPath);
|
|
243
|
+
const previous = await readFileBufferIfExists(targetPath);
|
|
244
|
+
const existingManaged = existingManagedFilesByPath.get(relativeTargetPath);
|
|
245
|
+
|
|
246
|
+
if (mutation.ownership === "app" && existingManaged) {
|
|
247
|
+
managedFiles.push({
|
|
248
|
+
...existingManaged,
|
|
249
|
+
path: relativeTargetPath,
|
|
250
|
+
preserveOnRemove: mutation.preserveOnRemove,
|
|
251
|
+
reason: mutation.reason || String(existingManaged.reason || ""),
|
|
252
|
+
category: mutation.category || String(existingManaged.category || ""),
|
|
253
|
+
id: mutation.id || String(existingManaged.id || "")
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (mutation.ownership === "app" && previous.exists && hashBuffer(previous.buffer) === renderedSourceHash) {
|
|
259
|
+
managedFiles.push({
|
|
260
|
+
path: relativeTargetPath,
|
|
261
|
+
hash: renderedSourceHash,
|
|
262
|
+
hadPrevious: true,
|
|
263
|
+
previousContentBase64: previous.buffer.toString("base64"),
|
|
264
|
+
preserveOnRemove: mutation.preserveOnRemove,
|
|
265
|
+
reason: mutation.reason,
|
|
266
|
+
category: mutation.category,
|
|
267
|
+
id: mutation.id
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
273
|
+
await writeFile(targetPath, renderedSourceContent, "utf8");
|
|
274
|
+
|
|
275
|
+
managedFiles.push({
|
|
276
|
+
path: relativeTargetPath,
|
|
277
|
+
hash: renderedSourceHash,
|
|
278
|
+
hadPrevious: previous.exists,
|
|
279
|
+
previousContentBase64: previous.exists ? previous.buffer.toString("base64") : "",
|
|
280
|
+
preserveOnRemove: mutation.preserveOnRemove,
|
|
281
|
+
reason: mutation.reason,
|
|
282
|
+
category: mutation.category,
|
|
283
|
+
id: mutation.id
|
|
284
|
+
});
|
|
285
|
+
touchedFiles.add(relativeTargetPath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
243
288
|
}
|
|
244
289
|
|
|
245
290
|
export {
|
|
246
291
|
applyFileMutations,
|
|
247
|
-
|
|
292
|
+
prepareFileMutations
|
|
248
293
|
};
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { createCliError } from "../../shared/cliError.js";
|
|
8
8
|
import { ensureObject } from "../../shared/collectionUtils.js";
|
|
9
|
-
import { interpolateOptionValue } from "../../shared/optionInterpolation.js";
|
|
10
9
|
import {
|
|
11
10
|
hashBuffer,
|
|
12
11
|
normalizeMigrationExtension,
|
|
@@ -19,25 +18,18 @@ import {
|
|
|
19
18
|
fileExists
|
|
20
19
|
} from "../ioAndMigrations.js";
|
|
21
20
|
import { normalizeRelativePosixPath } from "../localPackageSupport.js";
|
|
22
|
-
import {
|
|
23
|
-
applyTemplateContextReplacements,
|
|
24
|
-
resolveTemplateContextReplacementsForMutation
|
|
25
|
-
} from "./templateContext.js";
|
|
26
21
|
|
|
27
22
|
async function applyInstallMigrationMutation({
|
|
28
23
|
packageEntry,
|
|
29
|
-
|
|
30
|
-
rawMutation,
|
|
31
|
-
mutationIndex,
|
|
32
|
-
options,
|
|
24
|
+
preparedMutation,
|
|
33
25
|
appRoot,
|
|
34
26
|
managedMigrations,
|
|
35
27
|
managedMigrationById,
|
|
36
28
|
touchedFiles,
|
|
37
|
-
warnings
|
|
38
|
-
precomputedTemplateContextByMutationIndex
|
|
29
|
+
warnings
|
|
39
30
|
} = {}) {
|
|
40
|
-
|
|
31
|
+
const mutation = ensureObject(preparedMutation?.mutation);
|
|
32
|
+
if (mutation.preserveOnRemove === true) {
|
|
41
33
|
warnings.push(
|
|
42
34
|
`${packageEntry.packageId}: install-migration ignores preserveOnRemove (migrations are always preserved on remove).`
|
|
43
35
|
);
|
|
@@ -45,36 +37,17 @@ async function applyInstallMigrationMutation({
|
|
|
45
37
|
|
|
46
38
|
const from = mutation.from;
|
|
47
39
|
const toDir = mutation.toDir || "migrations";
|
|
40
|
+
const sourcePath = String(preparedMutation?.sourcePath || "").trim();
|
|
41
|
+
const renderedSourceContent = preparedMutation?.renderedSourceContent;
|
|
48
42
|
if (!from) {
|
|
49
43
|
throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: \"from\" is required.`);
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const sourcePath = path.join(packageEntry.rootDir, from);
|
|
54
|
-
if (!(await fileExists(sourcePath))) {
|
|
55
|
-
throw createCliError(`Missing migration template source ${sourcePath} for ${packageEntry.packageId}.`);
|
|
45
|
+
if (!sourcePath) {
|
|
46
|
+
throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: missing prepared source path.`);
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
precomputedTemplateContextByMutationIndex.has(mutationIndex);
|
|
61
|
-
const templateContextReplacements = hasPrecomputedTemplateContext
|
|
62
|
-
? precomputedTemplateContextByMutationIndex.get(mutationIndex)
|
|
63
|
-
: await resolveTemplateContextReplacementsForMutation({
|
|
64
|
-
packageEntry,
|
|
65
|
-
mutation,
|
|
66
|
-
options,
|
|
67
|
-
appRoot,
|
|
68
|
-
sourcePath,
|
|
69
|
-
targetPaths: [path.join(appRoot, toDir)]
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const sourceContent = await readFile(sourcePath, "utf8");
|
|
73
|
-
let renderedSourceContent = sourceContent.includes("${")
|
|
74
|
-
? interpolateOptionValue(sourceContent, options, packageEntry.packageId, `${mutation.id || from}.source`)
|
|
75
|
-
: sourceContent;
|
|
76
|
-
if (templateContextReplacements) {
|
|
77
|
-
renderedSourceContent = applyTemplateContextReplacements(renderedSourceContent, templateContextReplacements);
|
|
48
|
+
const migrationId = normalizeMigrationId(mutation.id, packageEntry.packageId);
|
|
49
|
+
if (typeof renderedSourceContent !== "string") {
|
|
50
|
+
throw createCliError(`Invalid install-migration mutation in ${packageEntry.packageId}: missing rendered migration source.`);
|
|
78
51
|
}
|
|
79
52
|
const sourceExtension = normalizeMigrationExtension(path.extname(from), ".cjs");
|
|
80
53
|
const extension = normalizeMigrationExtension(mutation.extension, sourceExtension);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
readFile,
|
|
4
|
-
writeFile
|
|
5
|
-
} from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { pathToFileURL } from "node:url";
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { importFreshModuleFromAbsolutePath } from "@jskit-ai/kernel/server/support";
|
|
8
3
|
import { createCliError } from "../../shared/cliError.js";
|
|
9
4
|
import {
|
|
10
5
|
ensureArray,
|
|
@@ -31,6 +26,8 @@ function interpolateFileMutationRecord(mutation, options, packageId) {
|
|
|
31
26
|
toSurfacePath: interpolate(mutation.toSurfacePath, "toSurfacePath"),
|
|
32
27
|
toDir: interpolate(mutation.toDir, "toDir"),
|
|
33
28
|
extension: interpolate(mutation.extension, "extension"),
|
|
29
|
+
ownership: interpolate(mutation.ownership, "ownership"),
|
|
30
|
+
expectedExistingFrom: interpolate(mutation.expectedExistingFrom, "expectedExistingFrom"),
|
|
34
31
|
id: interpolate(mutation.id, "id"),
|
|
35
32
|
category: interpolate(mutation.category, "category"),
|
|
36
33
|
reason: interpolate(mutation.reason, "reason"),
|
|
@@ -55,9 +52,8 @@ function applyTemplateContextReplacements(sourceContent, replacements) {
|
|
|
55
52
|
return output;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
async function
|
|
55
|
+
async function renderTemplateFile(
|
|
59
56
|
sourcePath,
|
|
60
|
-
targetPath,
|
|
61
57
|
options,
|
|
62
58
|
packageId,
|
|
63
59
|
interpolationKey,
|
|
@@ -70,9 +66,7 @@ async function copyTemplateFile(
|
|
|
70
66
|
if (templateContextReplacements) {
|
|
71
67
|
renderedContent = applyTemplateContextReplacements(renderedContent, templateContextReplacements);
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
75
|
-
await writeFile(targetPath, renderedContent, "utf8");
|
|
69
|
+
return renderedContent;
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
async function resolveTemplateContextReplacementsForMutation({
|
|
@@ -111,7 +105,7 @@ async function resolveTemplateContextReplacementsForMutation({
|
|
|
111
105
|
|
|
112
106
|
let moduleNamespace = null;
|
|
113
107
|
try {
|
|
114
|
-
moduleNamespace = await
|
|
108
|
+
moduleNamespace = await importFreshModuleFromAbsolutePath(absoluteEntrypointPath);
|
|
115
109
|
} catch (error) {
|
|
116
110
|
throw createCliError(
|
|
117
111
|
`Unable to load templateContext entrypoint ${entrypoint} for ${packageEntry.packageId}: ${String(error?.message || error || "unknown error")}`
|
|
@@ -165,7 +159,7 @@ async function resolveTemplateContextReplacementsForMutation({
|
|
|
165
159
|
|
|
166
160
|
export {
|
|
167
161
|
applyTemplateContextReplacements,
|
|
168
|
-
copyTemplateFile,
|
|
169
162
|
interpolateFileMutationRecord,
|
|
163
|
+
renderTemplateFile,
|
|
170
164
|
resolveTemplateContextReplacementsForMutation
|
|
171
165
|
};
|
|
@@ -118,25 +118,30 @@ async function applyTextMutations(packageEntry, appRoot, textMutations, options,
|
|
|
118
118
|
const operation = String(mutation?.op || "").trim();
|
|
119
119
|
if (operation === "upsert-env") {
|
|
120
120
|
const relativeFile = String(mutation?.file || "").trim();
|
|
121
|
-
const
|
|
122
|
-
if (!relativeFile || !
|
|
121
|
+
const rawKey = String(mutation?.key || "").trim();
|
|
122
|
+
if (!relativeFile || !rawKey) {
|
|
123
123
|
throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: "file" and "key" are required.`);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
const resolvedKey = interpolateOptionValue(rawKey, options, packageEntry.packageId, `${rawKey}.key`).trim();
|
|
127
|
+
if (!resolvedKey) {
|
|
128
|
+
throw createCliError(`Invalid upsert-env mutation in ${packageEntry.packageId}: resolved key is empty.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
const absoluteFile = path.join(appRoot, relativeFile);
|
|
127
132
|
const previous = await readFileBufferIfExists(absoluteFile);
|
|
128
133
|
const previousContent = previous.exists ? previous.buffer.toString("utf8") : "";
|
|
129
|
-
const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId,
|
|
130
|
-
const upserted = upsertEnvValue(previousContent,
|
|
134
|
+
const resolvedValue = interpolateOptionValue(mutation?.value || "", options, packageEntry.packageId, resolvedKey);
|
|
135
|
+
const upserted = upsertEnvValue(previousContent, resolvedKey, resolvedValue);
|
|
131
136
|
|
|
132
137
|
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
133
138
|
await writeFile(absoluteFile, upserted.content, "utf8");
|
|
134
139
|
|
|
135
|
-
const recordKey = `${relativeFile}::${String(mutation?.id ||
|
|
140
|
+
const recordKey = `${relativeFile}::${String(mutation?.id || resolvedKey)}`;
|
|
136
141
|
managedText[recordKey] = {
|
|
137
142
|
file: relativeFile,
|
|
138
143
|
op: "upsert-env",
|
|
139
|
-
key,
|
|
144
|
+
key: resolvedKey,
|
|
140
145
|
value: resolvedValue,
|
|
141
146
|
hadPrevious: upserted.hadPrevious,
|
|
142
147
|
previousValue: upserted.previousValue,
|