diff --git a/cli/src/commands/app/apps.ts b/cli/src/commands/app/apps.ts index 8255532bee..91933c0f02 100644 --- a/cli/src/commands/app/apps.ts +++ b/cli/src/commands/app/apps.ts @@ -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 = v as any; if (o["content"] && o["content"].startsWith("!inline")) { diff --git a/cli/src/commands/sync/global.ts b/cli/src/commands/sync/global.ts new file mode 100644 index 0000000000..bd64e1452e --- /dev/null +++ b/cli/src/commands/sync/global.ts @@ -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 + ); +} diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 4282b42298..8fffa5ed32 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -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) { 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) { 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 = 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 { - 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(); @@ -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 = {}; 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 = await getRawWorkspaceDependencies(); + const rawWorkspaceDependencies: Record = + 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")