fix(cli): handle better apps pushing back-compatibility

This commit is contained in:
Ruben Fiszel
2025-12-02 12:47:51 +00:00
parent 67e30c7741
commit dc9d25289b
3 changed files with 173 additions and 73 deletions
+7 -2
View File
@@ -16,6 +16,7 @@ import { ListableApp, Policy } from "../../../gen/types.gen.ts";
import { GlobalOptions, isSuperset } from "../../types.ts";
import { readInlinePathSync } from "../../utils/utils.ts";
import devCommand from "./dev.ts";
import { isVersionsGeq15851 } from "../sync/global.ts";
export interface AppFile {
value: any;
@@ -58,9 +59,13 @@ export function replaceInlineScripts(rec: any, localPath: string) {
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == "runType") {
rec["type"] = "path";
if (isVersionsGeq15851()) {
rec["type"] = "path";
}
} else if (k == "inlineScript" && typeof v == "object") {
rec["type"] = "inline";
if (isVersionsGeq15851()) {
rec["type"] = "inline";
}
const o: Record<string, any> = v as any;
if (o["content"] && o["content"].startsWith("!inline")) {
+31
View File
@@ -0,0 +1,31 @@
import { colors, log } from "../../../deps.ts";
let GLOBAL_VERSIONS: {
remoteMajor: number | undefined;
remoteMinor: number | undefined;
} = {
remoteMajor: undefined,
remoteMinor: undefined,
};
export function updateGlobalVersions(version: string) {
try {
const [prefix, remoteMinorStr] = version.split(".");
GLOBAL_VERSIONS = {
remoteMajor: parseInt(prefix.split("v")[1]),
remoteMinor: parseInt(remoteMinorStr),
};
} catch (e) {
log.info(colors.gray(`Error reading remote version: ${e}`));
}
}
export function isVersionsGeq15851(): boolean {
return (
GLOBAL_VERSIONS.remoteMajor !== undefined &&
GLOBAL_VERSIONS.remoteMajor >= 1 &&
GLOBAL_VERSIONS.remoteMinor !== undefined &&
GLOBAL_VERSIONS.remoteMinor >= 5851
);
}
+135 -71
View File
@@ -34,7 +34,12 @@ import {
} from "../script/script.ts";
import { handleFile } from "../script/script.ts";
import { deepEqual, isFileResource, isRawAppFile, isWorkspaceDependencies } from "../../utils/utils.ts";
import {
deepEqual,
isFileResource,
isRawAppFile,
isWorkspaceDependencies,
} from "../../utils/utils.ts";
import {
getEffectiveSettings,
mergeConfigWithConfigFile,
@@ -70,6 +75,7 @@ import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../w
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { isExecutionModeAnonymous } from "../app/apps.ts";
import { generateAppLocksInternal } from "../app/app_metadata.ts";
import { updateGlobalVersions } from "./global.ts";
// Merge CLI options with effective settings, preserving CLI flags as overrides
function mergeCliWithEffectiveOptions<
@@ -268,29 +274,28 @@ function extractFields(fields: Record<string, any>) {
Object.entries(fields).forEach(([k, v]) => {
if (typeof v == "object") {
if (v.type == "static") {
fields[k] = { value: v.value }
fields[k] = { value: v.value };
} else if (v.type == "javascript") {
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources }
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources };
} else if (v.type == "user") {
fields[k] = undefined
fields[k] = undefined;
}
}
// if (k == 'runType') {
// fields["type"] = undefined
// fields["schema"] = undefined
// }
})
});
}
export function extractFieldsForRawApps(runnables: Record<string, any>) {
Object.values(runnables).forEach((v) => {
if (typeof v == "object") {
if (v.fields !== undefined) {
extractFields(v.fields)
extractFields(v.fields);
}
}
})
});
}
export function extractInlineScriptsForApps(
key: string | undefined,
@@ -303,12 +308,12 @@ export function extractInlineScriptsForApps(
}
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == 'runType') {
rec["type"] = undefined
rec["schema"] = undefined
return []
if (k == "runType") {
rec["type"] = undefined;
rec["schema"] = undefined;
return [];
} else if (k == "inlineScript" && typeof v == "object") {
rec["type"] = undefined
rec["type"] = undefined;
const o: Record<string, any> = v as any;
const name = toId(key ?? "", rec);
const [basePathO, ext] = pathAssigner.assignPath(name, o["language"]);
@@ -351,20 +356,26 @@ function ZipFSElement(
p: string,
f: JSZip.JSZipObject
): Promise<DynFSElement[]> {
const kind: "flow" | "app" | "script" | "resource" | "other" | "raw_app" | "dependencies" =
p.endsWith(".flow.json")
? "flow"
: p.endsWith(".app.json")
? "app"
: p.endsWith(".raw_app.json")
? "raw_app"
: p.endsWith(".script.json")
? "script"
: p.endsWith(".resource.json")
? "resource"
: p.startsWith("dependencies/")
const kind:
| "flow"
| "app"
| "script"
| "resource"
| "other"
| "raw_app"
| "dependencies" = p.endsWith(".flow.json")
? "flow"
: p.endsWith(".app.json")
? "app"
: p.endsWith(".raw_app.json")
? "raw_app"
: p.endsWith(".script.json")
? "script"
: p.endsWith(".resource.json")
? "resource"
: p.startsWith("dependencies/")
? "dependencies"
: "other";
: "other";
const isJson = p.endsWith(".json");
@@ -502,7 +513,7 @@ function ZipFSElement(
undefined,
value,
newPathAssigner(defaultTs),
(key, val_) => key
(key, val_) => key
);
} catch (error) {
log.error(
@@ -759,7 +770,11 @@ export async function* readDirRecursiveWithIgnore(
for await (const e2 of e.c()) {
if (e2.isDirectory) {
const dirName = e2.path.split(SEP).pop();
if (dirName == "node_modules" || dirName == ".claude" || dirName?.startsWith(".")) {
if (
dirName == "node_modules" ||
dirName == ".claude" ||
dirName?.startsWith(".")
) {
continue;
}
}
@@ -792,7 +807,7 @@ export async function elementsToMap(
ignore: (path: string, isDirectory: boolean) => boolean,
json: boolean,
skips: Skips,
specificItems?: SpecificItemsConfig,
specificItems?: SpecificItemsConfig
): Promise<{ [key: string]: string }> {
const map: { [key: string]: string } = {};
const processedBasePaths = new Set<string>();
@@ -802,7 +817,11 @@ export async function elementsToMap(
continue;
}
const path = entry.path;
if (!isFileResource(path) && !isRawAppFile(path) && !isWorkspaceDependencies(path)) {
if (
!isFileResource(path) &&
!isRawAppFile(path) &&
!isWorkspaceDependencies(path)
) {
if (json && path.endsWith(".yaml")) continue;
if (!json && path.endsWith(".json")) continue;
@@ -835,7 +854,11 @@ export async function elementsToMap(
if (isRawAppFile(path)) {
const suffix = path.split(".raw_app" + SEP).pop();
if (suffix?.startsWith("dist/") || suffix == "wmill.d.ts" || suffix == "package-lock.json") {
if (
suffix?.startsWith("dist/") ||
suffix == "wmill.d.ts" ||
suffix == "package-lock.json"
) {
continue;
}
}
@@ -867,7 +890,6 @@ export async function elementsToMap(
continue;
}
// Use getTypeStrFromPath for consistent type detection
try {
const fileType = getTypeStrFromPath(path);
@@ -876,12 +898,15 @@ export async function elementsToMap(
if (skips.skipFlows && fileType === "flow") continue;
if (skips.skipApps && fileType === "app") continue;
if (skips.skipFolders && fileType === "folder") continue;
if (skips.skipWorkspaceDependencies && fileType === "workspace_dependencies") continue;
if (
skips.skipWorkspaceDependencies &&
fileType === "workspace_dependencies"
)
continue;
} catch {
// If getTypeStrFromPath can't determine the type, continue processing the file
}
// Handle branch-specific files - skip files for other branches
if (specificItems && isBranchSpecificFile(path)) {
const currentBranch = getCurrentGitBranch();
@@ -945,7 +970,7 @@ export async function elementsToMap(
// No specific items configuration, use regular path
map[entry.path] = content;
}
}
}
return map;
}
@@ -1090,7 +1115,6 @@ async function compareDynFSElement(
const remoteCodebase: Record<string, string> = {};
for (const [k] of Object.entries(m2)) {
if (m1[k] === undefined) {
if (
!ignoreMetadataDeletion ||
@@ -1239,7 +1263,7 @@ export const isWhitelisted = (p: string) => {
p == "f" ||
p == "g" ||
p == "users" ||
p == "groups" ||
p == "groups" ||
p == "dependencies"
);
};
@@ -1308,7 +1332,10 @@ export async function ignoreF(wmillconf: {
if (wmillconf.includeKey && fileType === "encryption_key") {
return false; // Don't ignore, always include
}
if (!wmillconf.skipWorkspaceDependencies && fileType === "workspace_dependencies") {
if (
!wmillconf.skipWorkspaceDependencies &&
fileType === "workspace_dependencies"
) {
return false; // Don't ignore workspace dependencies (they are always included unless explicitly skipped)
}
} catch {
@@ -1440,22 +1467,22 @@ export async function pull(
// ignore
}
const zipFile = await downloadZip(
workspace,
opts.plainSecrets,
opts.skipVariables,
opts.skipResources,
opts.skipResourceTypes,
opts.skipSecrets,
opts.includeSchedules,
opts.includeTriggers,
opts.includeUsers,
opts.includeGroups,
opts.includeSettings,
opts.includeKey,
opts.skipWorkspaceDependencies,
opts.defaultTs
);
workspace,
opts.plainSecrets,
opts.skipVariables,
opts.skipResources,
opts.skipResourceTypes,
opts.skipSecrets,
opts.includeSchedules,
opts.includeTriggers,
opts.includeUsers,
opts.includeGroups,
opts.includeSettings,
opts.includeKey,
opts.skipWorkspaceDependencies,
opts.defaultTs
);
const remote = ZipFSElement(
zipFile!,
!opts.json,
@@ -1482,7 +1509,6 @@ export async function pull(
`remote (${workspace.name}) -> local: ${changes.length} changes to apply`
);
// Handle JSON output for dry-run
if (opts.dryRun && opts.jsonOutput) {
const result = {
@@ -1671,7 +1697,8 @@ export async function pull(
await readLockfile(); // ensure wmill-lock.yaml exists
const tracker: ChangeTracker = await buildTracker(changes);
const rawWorkspaceDependencies: Record<string, string> = await getRawWorkspaceDependencies();
const rawWorkspaceDependencies: Record<string, string> =
await getRawWorkspaceDependencies();
for (const change of tracker.scripts) {
await generateScriptMetadataInternal(
@@ -1687,7 +1714,14 @@ export async function pull(
}
for (const change of tracker.flows) {
log.info(`Updating lock for flow ${change}`);
await generateFlowLockInternal(change, false, workspace, opts, true, false);
await generateFlowLockInternal(
change,
false,
workspace,
opts,
true,
false
);
}
if (tracker.apps.length > 0) {
log.info(
@@ -1700,7 +1734,14 @@ export async function pull(
}
for (const change of tracker.rawApps) {
log.info(`Updating lock metadata for raw app ${change}`);
await generateAppLocksInternal(change, false, workspace, opts, true, true);
await generateAppLocksInternal(
change,
false,
workspace,
opts,
true,
true
);
}
if (opts.jsonOutput) {
const result = {
@@ -1899,6 +1940,7 @@ export async function push(
} catch {
// ignore
}
const remote = ZipFSElement(
(await downloadZip(
workspace,
@@ -1935,13 +1977,13 @@ export async function push(
specificItems
);
const rawWorkspaceDependencies = await getRawWorkspaceDependencies();
const tracker: ChangeTracker = await buildTracker(changes);
const staleScripts: string[] = [];
const staleFlows: string[] = [];
for (const change of tracker.scripts) {
const stale = await generateScriptMetadataInternal(
change,
@@ -1995,7 +2037,9 @@ export async function push(
}
const version = await fetchVersion(workspace.remote);
if (version) {
updateGlobalVersions(version);
}
log.info(colors.gray("Remote version: " + version));
log.info(
@@ -2097,14 +2141,18 @@ export async function push(
const alreadySynced: string[] = [];
const isRawApp = isRawAppFile(changes[0].path);
if (isRawApp) {
const deleteRawApp = changes.find(change => change.name === "deleted" && change.path.endsWith(".raw_app/raw_app.yaml"))
const deleteRawApp = changes.find(
(change) =>
change.name === "deleted" &&
change.path.endsWith(".raw_app/raw_app.yaml")
);
if (deleteRawApp) {
changes = [deleteRawApp];
} else {
changes.splice(1, changes.length - 1);
}
}
for await (const change of changes) {
let stateTarget = undefined;
if (stateful) {
@@ -2324,13 +2372,16 @@ export async function push(
});
break;
case "raw_app":
if (target.endsWith(".raw_app/raw_app.yaml") || target.endsWith(".raw_app/raw_app.json")) {
if (
target.endsWith(".raw_app/raw_app.yaml") ||
target.endsWith(".raw_app/raw_app.json")
) {
await wmill.deleteApp({
workspace: workspaceId,
path: removeSuffix(target, ".raw_app/raw_app.json"),
});
}
break;
break;
case "schedule":
await wmill.deleteSchedule({
workspace: workspaceId,
@@ -2426,18 +2477,25 @@ export async function push(
});
break;
case "workspace_dependencies":
const relativePath = removePathPrefix(change.path, "dependencies");
const res = workspaceDependenciesPathToLanguageAndFilename(change.path);
const relativePath = removePathPrefix(
change.path,
"dependencies"
);
const res = workspaceDependenciesPathToLanguageAndFilename(
change.path
);
if (!res) {
throw new Error(`Unknown workspace dependencies file format: ${change.path}`);
throw new Error(
`Unknown workspace dependencies file format: ${change.path}`
);
}
const { name, language } = res;
await wmill.deleteWorkspaceDependencies({
workspace: workspaceId,
language,
name
name,
});
break;
@@ -2537,7 +2595,10 @@ const command = new Command()
.option("--skip-flows", "Skip syncing flows")
.option("--skip-apps", "Skip syncing apps")
.option("--skip-folders", "Skip syncing folders")
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
.option(
"--skip-workspace-dependencies",
"Skip syncing workspace dependencies"
)
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-triggers", "Include syncing triggers")
@@ -2586,7 +2647,10 @@ const command = new Command()
.option("--skip-flows", "Skip syncing flows")
.option("--skip-apps", "Skip syncing apps")
.option("--skip-folders", "Skip syncing folders")
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
.option(
"--skip-workspace-dependencies",
"Skip syncing workspace dependencies"
)
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-triggers", "Include syncing triggers")