windmill-cli 1.681.0 → 1.683.0

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 (3) hide show
  1. package/README.md +22 -0
  2. package/esm/main.js +887 -162
  3. package/package.json +1 -1
package/esm/main.js CHANGED
@@ -11812,7 +11812,7 @@ var init_OpenAPI = __esm(() => {
11812
11812
  PASSWORD: undefined,
11813
11813
  TOKEN: getEnv2("WM_TOKEN"),
11814
11814
  USERNAME: undefined,
11815
- VERSION: "1.681.0",
11815
+ VERSION: "1.683.0",
11816
11816
  WITH_CREDENTIALS: true,
11817
11817
  interceptors: {
11818
11818
  request: new Interceptors,
@@ -12350,6 +12350,7 @@ __export(exports_services_gen, {
12350
12350
  listExtJwtTokens: () => listExtJwtTokens,
12351
12351
  listEmailTriggers: () => listEmailTriggers,
12352
12352
  listDucklakes: () => listDucklakes,
12353
+ listDeploymentRequestEligibleDeployers: () => listDeploymentRequestEligibleDeployers,
12353
12354
  listDedicatedWithDeps: () => listDedicatedWithDeps,
12354
12355
  listDataTables: () => listDataTables,
12355
12356
  listDataTableSchemas: () => listDataTableSchemas,
@@ -12434,6 +12435,7 @@ __export(exports_services_gen, {
12434
12435
  getScheduledFor: () => getScheduledFor,
12435
12436
  getSchedule: () => getSchedule,
12436
12437
  getRunnable: () => getRunnable,
12438
+ getRuffConfig: () => getRuffConfig,
12437
12439
  getRootJobId: () => getRootJobId,
12438
12440
  getResumeUrls: () => getResumeUrls,
12439
12441
  getResourceValueInterpolated: () => getResourceValueInterpolated,
@@ -12453,6 +12455,7 @@ __export(exports_services_gen, {
12453
12455
  getPostgresVersion: () => getPostgresVersion,
12454
12456
  getPostgresTrigger: () => getPostgresTrigger,
12455
12457
  getPostgresPublication: () => getPostgresPublication,
12458
+ getOpenDeploymentRequest: () => getOpenDeploymentRequest,
12456
12459
  getOpenApiYaml: () => getOpenApiYaml,
12457
12460
  getOidcToken: () => getOidcToken,
12458
12461
  getObjectStorageUsage: () => getObjectStorageUsage,
@@ -12716,6 +12719,8 @@ __export(exports_services_gen, {
12716
12719
  createFlow: () => createFlow,
12717
12720
  createEmailTrigger: () => createEmailTrigger,
12718
12721
  createDraft: () => createDraft,
12722
+ createDeploymentRequestComment: () => createDeploymentRequestComment,
12723
+ createDeploymentRequest: () => createDeploymentRequest,
12719
12724
  createCustomerPortalSession: () => createCustomerPortalSession,
12720
12725
  createAppRaw: () => createAppRaw,
12721
12726
  createApp: () => createApp,
@@ -12733,6 +12738,7 @@ __export(exports_services_gen, {
12733
12738
  computeObjectStorageUsage: () => computeObjectStorageUsage,
12734
12739
  compareWorkspaces: () => compareWorkspaces,
12735
12740
  commitKafkaOffsets: () => commitKafkaOffsets,
12741
+ closeDeploymentRequestMerged: () => closeDeploymentRequestMerged,
12736
12742
  clearIndex: () => clearIndex,
12737
12743
  checkS3FolderExists: () => checkS3FolderExists,
12738
12744
  checkInstanceSharingAvailable: () => checkInstanceSharingAvailable,
@@ -12746,6 +12752,7 @@ __export(exports_services_gen, {
12746
12752
  cancelSelection: () => cancelSelection,
12747
12753
  cancelQueuedJob: () => cancelQueuedJob,
12748
12754
  cancelPersistentQueuedJobs: () => cancelPersistentQueuedJobs,
12755
+ cancelDeploymentRequest: () => cancelDeploymentRequest,
12749
12756
  blacklistAgentToken: () => blacklistAgentToken,
12750
12757
  batchReRunJobs: () => batchReRunJobs,
12751
12758
  backendVersion: () => backendVersion,
@@ -13191,6 +13198,11 @@ var backendVersion = () => {
13191
13198
  body: data2.requestBody,
13192
13199
  mediaType: "application/json"
13193
13200
  });
13201
+ }, getRuffConfig = () => {
13202
+ return request(OpenAPI, {
13203
+ method: "GET",
13204
+ url: "/settings_u/ruff_config"
13205
+ });
13194
13206
  }, getLocal = () => {
13195
13207
  return request(OpenAPI, {
13196
13208
  method: "GET",
@@ -14544,6 +14556,68 @@ var backendVersion = () => {
14544
14556
  404: "protection rule not found"
14545
14557
  }
14546
14558
  });
14559
+ }, listDeploymentRequestEligibleDeployers = (data2) => {
14560
+ return request(OpenAPI, {
14561
+ method: "GET",
14562
+ url: "/w/{workspace}/deployment_request/eligible_deployers",
14563
+ path: {
14564
+ workspace: data2.workspace
14565
+ }
14566
+ });
14567
+ }, getOpenDeploymentRequest = (data2) => {
14568
+ return request(OpenAPI, {
14569
+ method: "GET",
14570
+ url: "/w/{workspace}/deployment_request/open",
14571
+ path: {
14572
+ workspace: data2.workspace
14573
+ }
14574
+ });
14575
+ }, createDeploymentRequest = (data2) => {
14576
+ return request(OpenAPI, {
14577
+ method: "POST",
14578
+ url: "/w/{workspace}/deployment_request",
14579
+ path: {
14580
+ workspace: data2.workspace
14581
+ },
14582
+ body: data2.requestBody,
14583
+ mediaType: "application/json",
14584
+ errors: {
14585
+ 400: "invalid assignees",
14586
+ 409: "a deployment request is already open for this fork"
14587
+ }
14588
+ });
14589
+ }, cancelDeploymentRequest = (data2) => {
14590
+ return request(OpenAPI, {
14591
+ method: "POST",
14592
+ url: "/w/{workspace}/deployment_request/{id}/cancel",
14593
+ path: {
14594
+ workspace: data2.workspace,
14595
+ id: data2.id
14596
+ }
14597
+ });
14598
+ }, closeDeploymentRequestMerged = (data2) => {
14599
+ return request(OpenAPI, {
14600
+ method: "POST",
14601
+ url: "/w/{workspace}/deployment_request/{id}/close_merged",
14602
+ path: {
14603
+ workspace: data2.workspace,
14604
+ id: data2.id
14605
+ }
14606
+ });
14607
+ }, createDeploymentRequestComment = (data2) => {
14608
+ return request(OpenAPI, {
14609
+ method: "POST",
14610
+ url: "/w/{workspace}/deployment_request/{id}/comment",
14611
+ path: {
14612
+ workspace: data2.workspace,
14613
+ id: data2.id
14614
+ },
14615
+ body: data2.requestBody,
14616
+ mediaType: "application/json",
14617
+ errors: {
14618
+ 400: "invalid input or request closed"
14619
+ }
14620
+ });
14547
14621
  }, logAiChat = (data2) => {
14548
14622
  return request(OpenAPI, {
14549
14623
  method: "POST",
@@ -26322,6 +26396,7 @@ __export(exports_conf, {
26322
26396
  showDiffs: () => showDiffs,
26323
26397
  setShowDiffs: () => setShowDiffs,
26324
26398
  readConfigFile: () => readConfigFile,
26399
+ parseSyncBehavior: () => parseSyncBehavior,
26325
26400
  mergeConfigWithConfigFile: () => mergeConfigWithConfigFile,
26326
26401
  getWorkspaceNames: () => getWorkspaceNames,
26327
26402
  getWmillYamlPath: () => getWmillYamlPath,
@@ -26330,6 +26405,7 @@ __export(exports_conf, {
26330
26405
  getEffectiveGitBranch: () => getEffectiveGitBranch,
26331
26406
  findWorkspaceByGitBranch: () => findWorkspaceByGitBranch,
26332
26407
  convertGitBranchesToWorkspaces: () => convertGitBranchesToWorkspaces,
26408
+ SUPPORTED_SYNC_BEHAVIOR_VERSION: () => SUPPORTED_SYNC_BEHAVIOR_VERSION,
26333
26409
  GLOBAL_CONFIG_OPT: () => GLOBAL_CONFIG_OPT,
26334
26410
  DEFAULT_SYNC_OPTIONS: () => DEFAULT_SYNC_OPTIONS
26335
26411
  });
@@ -26340,6 +26416,13 @@ import { execSync as execSync2 } from "node:child_process";
26340
26416
  function setShowDiffs(value) {
26341
26417
  showDiffs = value;
26342
26418
  }
26419
+ function parseSyncBehavior(value) {
26420
+ if (!value && value !== 0)
26421
+ return 0;
26422
+ const s = String(value);
26423
+ const match = s.match(/^v(\d+)$/);
26424
+ return match ? parseInt(match[1], 10) : 0;
26425
+ }
26343
26426
  function getGitRepoRoot() {
26344
26427
  try {
26345
26428
  const result = execSync2("git rev-parse --show-toplevel", {
@@ -26441,6 +26524,11 @@ async function readConfigFile(opts) {
26441
26524
  warn("No defaultTs defined in your wmill.yaml. Using 'bun' as default.");
26442
26525
  }
26443
26526
  setNonDottedPaths(conf?.nonDottedPaths ?? false);
26527
+ const syncBehaviorVersion = parseSyncBehavior(conf?.syncBehavior);
26528
+ if (syncBehaviorVersion > SUPPORTED_SYNC_BEHAVIOR_VERSION) {
26529
+ error(`Your wmill.yaml specifies syncBehavior: ${conf.syncBehavior}, but this CLI only supports up to v${SUPPORTED_SYNC_BEHAVIOR_VERSION}. Run 'wmill upgrade' to update.`);
26530
+ process.exit(1);
26531
+ }
26444
26532
  return typeof conf == "object" ? conf : {};
26445
26533
  } catch (e) {
26446
26534
  if (e instanceof Error && (e.message.includes("overrides") || e.message.includes("Obsolete configuration format"))) {
@@ -26637,7 +26725,7 @@ function convertGitBranchesToWorkspaces(gitBranches) {
26637
26725
  }
26638
26726
  return workspaces;
26639
26727
  }
26640
- var import_yaml4, showDiffs = false, GLOBAL_CONFIG_OPT, legacyConfigWarned = false, DEFAULT_SYNC_OPTIONS, RESERVED_WORKSPACE_KEYS;
26728
+ var import_yaml4, showDiffs = false, SUPPORTED_SYNC_BEHAVIOR_VERSION = 1, GLOBAL_CONFIG_OPT, legacyConfigWarned = false, DEFAULT_SYNC_OPTIONS, RESERVED_WORKSPACE_KEYS;
26641
26729
  var init_conf = __esm(async () => {
26642
26730
  init_log();
26643
26731
  init_yaml();
@@ -26666,7 +26754,8 @@ var init_conf = __esm(async () => {
26666
26754
  includeSettings: false,
26667
26755
  includeKey: false,
26668
26756
  skipWorkspaceDependencies: false,
26669
- nonDottedPaths: false
26757
+ nonDottedPaths: false,
26758
+ syncBehavior: "v1"
26670
26759
  };
26671
26760
  RESERVED_WORKSPACE_KEYS = new Set(["commonSpecificItems"]);
26672
26761
  });
@@ -33910,6 +33999,22 @@ var escape2 = (s, { windowsPathsNoEscape = false, magicalBraces = false } = {})
33910
33999
  };
33911
34000
 
33912
34001
  // node_modules/minimatch/dist/esm/index.js
34002
+ var exports_esm = {};
34003
+ __export(exports_esm, {
34004
+ unescape: () => unescape2,
34005
+ sep: () => sep2,
34006
+ minimatch: () => minimatch,
34007
+ match: () => match,
34008
+ makeRe: () => makeRe,
34009
+ filter: () => filter,
34010
+ escape: () => escape2,
34011
+ defaults: () => defaults,
34012
+ braceExpand: () => braceExpand,
34013
+ Minimatch: () => Minimatch,
34014
+ GLOBSTAR: () => GLOBSTAR,
34015
+ AST: () => AST
34016
+ });
34017
+
33913
34018
  class Minimatch {
33914
34019
  options;
33915
34020
  set;
@@ -60930,17 +61035,17 @@ async function findResourceFile(path7) {
60930
61035
  }
60931
61036
  return validCandidates[0];
60932
61037
  }
60933
- async function handleScriptMetadata(path7, workspace, alreadySynced, message, rawWorkspaceDependencies, codebases, opts) {
61038
+ async function handleScriptMetadata(path7, workspace, alreadySynced, message, rawWorkspaceDependencies, codebases, opts, permissionedAsContext) {
60934
61039
  const isFlatMeta = path7.endsWith(".script.json") || path7.endsWith(".script.yaml") || path7.endsWith(".script.lock");
60935
61040
  const isFolderMeta = !isFlatMeta && isScriptModulePath(path7) && (path7.endsWith("/script.yaml") || path7.endsWith("/script.json") || path7.endsWith("/script.lock"));
60936
61041
  if (isFlatMeta || isFolderMeta) {
60937
61042
  const contentPath = await findContentFile(path7);
60938
- return handleFile(contentPath, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases);
61043
+ return handleFile(contentPath, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases, permissionedAsContext);
60939
61044
  } else {
60940
61045
  return false;
60941
61046
  }
60942
61047
  }
60943
- async function handleFile(path7, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases) {
61048
+ async function handleFile(path7, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases, permissionedAsContext) {
60944
61049
  const moduleEntryPoint = isModuleEntryPoint(path7);
60945
61050
  if (!isAppInlineScriptPath2(path7) && !isFlowInlineScriptPath2(path7) && !isRawAppBackendPath2(path7) && (!isScriptModulePath(path7) || moduleEntryPoint) && exts.some((exts) => path7.endsWith(exts))) {
60946
61051
  if (alreadySynced.includes(path7)) {
@@ -61085,10 +61190,19 @@ async function handleFile(path7, workspace, alreadySynced, message, opts, rawWor
61085
61190
  modules,
61086
61191
  labels: typed?.labels
61087
61192
  };
61193
+ const hasOnBehalfOf = typed?.has_on_behalf_of ?? !!typed?.on_behalf_of_email;
61194
+ delete typed?.has_on_behalf_of;
61195
+ if (permissionedAsContext?.userIsAdminOrDeployer && hasOnBehalfOf) {
61196
+ if (remote && remote.on_behalf_of_email) {
61197
+ requestBodyCommon.on_behalf_of_email = remote.on_behalf_of_email;
61198
+ requestBodyCommon.preserve_on_behalf_of = true;
61199
+ info(`Preserving ${remote.on_behalf_of_email} as on_behalf_of for script ${remotePath}`);
61200
+ }
61201
+ }
61088
61202
  if (remote) {
61089
61203
  if (content === remote.content) {
61090
61204
  if (typed == undefined || typed.description === remote.description && typed.summary === remote.summary && typed.kind == remote.kind && !remote.archived && (Array.isArray(remote?.lock) ? remote?.lock?.join(`
61091
- `) : remote?.lock ?? "").trim() == (typed?.lock ?? "").trim() && deepEqual(typed.schema, remote.schema) && typed.tag == remote.tag && (typed.ws_error_handler_muted ?? false) == remote.ws_error_handler_muted && typed.dedicated_worker == remote.dedicated_worker && typed.cache_ttl == remote.cache_ttl && typed.concurrency_time_window_s == remote.concurrency_time_window_s && typed.concurrent_limit == remote.concurrent_limit && Boolean(typed.restart_unless_cancelled) == Boolean(remote.restart_unless_cancelled) && Boolean(typed.visible_to_runner_only) == Boolean(remote.visible_to_runner_only) && Boolean(typed.has_preprocessor) == Boolean(remote.has_preprocessor) && typed.priority == Boolean(remote.priority) && typed.timeout == remote.timeout && typed.concurrency_key == remote["concurrency_key"] && typed.debounce_key == remote["debounce_key"] && typed.debounce_delay_s == remote["debounce_delay_s"] && typed.codebase == remote.codebase && typed.on_behalf_of_email == remote.on_behalf_of_email && deepEqual(typed.envs, remote.envs) && deepEqual(modules ?? null, remote.modules ?? null)) {
61205
+ `) : remote?.lock ?? "").trim() == (typed?.lock ?? "").trim() && deepEqual(typed.schema, remote.schema) && typed.tag == remote.tag && (typed.ws_error_handler_muted ?? false) == remote.ws_error_handler_muted && typed.dedicated_worker == remote.dedicated_worker && typed.cache_ttl == remote.cache_ttl && typed.concurrency_time_window_s == remote.concurrency_time_window_s && typed.concurrent_limit == remote.concurrent_limit && Boolean(typed.restart_unless_cancelled) == Boolean(remote.restart_unless_cancelled) && Boolean(typed.visible_to_runner_only) == Boolean(remote.visible_to_runner_only) && Boolean(typed.has_preprocessor) == Boolean(remote.has_preprocessor) && typed.priority == Boolean(remote.priority) && typed.timeout == remote.timeout && typed.concurrency_key == remote["concurrency_key"] && typed.debounce_key == remote["debounce_key"] && typed.debounce_delay_s == remote["debounce_delay_s"] && typed.codebase == remote.codebase && (hasOnBehalfOf ? true : typed.on_behalf_of_email == remote.on_behalf_of_email) && deepEqual(typed.envs, remote.envs) && deepEqual(modules ?? null, remote.modules ?? null)) {
61092
61206
  info(colors.green(`Script ${remotePath} is up to date`));
61093
61207
  return true;
61094
61208
  }
@@ -61805,6 +61919,28 @@ async function history(opts, scriptPath) {
61805
61919
  ])).render();
61806
61920
  }
61807
61921
  }
61922
+ async function setPermissionedAs(opts, scriptPath, email) {
61923
+ const workspace = await resolveWorkspace(opts);
61924
+ await requireLogin(opts);
61925
+ const remote = await getScriptByPath({
61926
+ workspace: workspace.workspaceId,
61927
+ path: scriptPath
61928
+ });
61929
+ if (!remote)
61930
+ throw new Error(`Script ${scriptPath} not found`);
61931
+ await createScript({
61932
+ workspace: workspace.workspaceId,
61933
+ requestBody: {
61934
+ ...remote,
61935
+ lock: Array.isArray(remote.lock) ? remote.lock.join(`
61936
+ `) : remote.lock ?? undefined,
61937
+ parent_hash: remote.hash,
61938
+ on_behalf_of_email: email,
61939
+ preserve_on_behalf_of: true
61940
+ }
61941
+ });
61942
+ info(colors.green(`Updated permissioned_as for script ${scriptPath} to ${email}`));
61943
+ }
61808
61944
  var import_yaml6, exts, POLL_INTERVAL_MS = 2000, languageAliases, command4, script_default;
61809
61945
  var init_script = __esm(async () => {
61810
61946
  init_colors2();
@@ -61862,7 +61998,7 @@ var init_script = __esm(async () => {
61862
61998
  languageAliases = {
61863
61999
  python: "python3"
61864
62000
  };
61865
- command4 = new Command().description("script related commands").option("--show-archived", "Show archived scripts instead of active ones").option("--json", "Output as JSON (for piping to jq)").action(list4).command("list", "list all scripts").option("--show-archived", "Show archived scripts instead of active ones").option("--json", "Output as JSON (for piping to jq)").action(list4).command("push", "push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)").arguments("<path:file>").option("--message <message:string>", "Deployment message").action(push2).command("get", "get a script's details").arguments("<path:file>").option("--json", "Output as JSON (for piping to jq)").action(get2).command("show", "show a script's content (alias for get)").arguments("<path:file>").action(show).command("run", "run a script by path").arguments("<path:file>").option("-d --data <data:file>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other then the final output. Useful for scripting.").action(run2).command("preview", "preview a local script without deploying it. Supports both regular and codebase scripts.").arguments("<path:file>").option("-d --data <data:file>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other than the final output. Useful for scripting.").action(preview).command("new", "create a new script").arguments("<path:file> <language:string>").option("--summary <summary:string>", "script summary").option("--description <description:string>", "script description").action(bootstrap).command("bootstrap", "create a new script (alias for new)").arguments("<path:file> <language:string>").option("--summary <summary:string>", "script summary").option("--description <description:string>", "script description").action(bootstrap).command("generate-metadata", 'DEPRECATED: re-generate script metadata. Use top-level "wmill generate-metadata" instead.').arguments("[script:file]").option("--yes", "Skip confirmation prompt").option("--dry-run", "Perform a dry run without making changes").option("--lock-only", "re-generate only the lock").option("--schema-only", "re-generate only script schema").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").action(generateMetadata).command("history", "show version history for a script").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(history);
62001
+ command4 = new Command().description("script related commands").option("--show-archived", "Show archived scripts instead of active ones").option("--json", "Output as JSON (for piping to jq)").action(list4).command("list", "list all scripts").option("--show-archived", "Show archived scripts instead of active ones").option("--json", "Output as JSON (for piping to jq)").action(list4).command("push", "push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)").arguments("<path:file>").option("--message <message:string>", "Deployment message").action(push2).command("get", "get a script's details").arguments("<path:file>").option("--json", "Output as JSON (for piping to jq)").action(get2).command("show", "show a script's content (alias for get)").arguments("<path:file>").action(show).command("run", "run a script by path").arguments("<path:file>").option("-d --data <data:file>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other then the final output. Useful for scripting.").action(run2).command("preview", "preview a local script without deploying it. Supports both regular and codebase scripts.").arguments("<path:file>").option("-d --data <data:file>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other than the final output. Useful for scripting.").action(preview).command("new", "create a new script").arguments("<path:file> <language:string>").option("--summary <summary:string>", "script summary").option("--description <description:string>", "script description").action(bootstrap).command("bootstrap", "create a new script (alias for new)").arguments("<path:file> <language:string>").option("--summary <summary:string>", "script summary").option("--description <description:string>", "script description").action(bootstrap).command("generate-metadata", 'DEPRECATED: re-generate script metadata. Use top-level "wmill generate-metadata" instead.').arguments("[script:file]").option("--yes", "Skip confirmation prompt").option("--dry-run", "Perform a dry run without making changes").option("--lock-only", "re-generate only the lock").option("--schema-only", "re-generate only script schema").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").action(generateMetadata).command("set-permissioned-as", "Set the on_behalf_of_email for a script (requires admin or wm_deployers group)").arguments("<path:string> <email:string>").action(setPermissionedAs).command("history", "show version history for a script").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(history);
61866
62002
  script_default = command4;
61867
62003
  });
61868
62004
 
@@ -62420,6 +62556,154 @@ var init_lint = __esm(async () => {
62420
62556
  lint_default = command5;
62421
62557
  });
62422
62558
 
62559
+ // src/core/permissioned_as.ts
62560
+ var exports_permissioned_as = {};
62561
+ __export(exports_permissioned_as, {
62562
+ preCheckPermissionedAs: () => preCheckPermissionedAs,
62563
+ lookupUsernameByEmail: () => lookupUsernameByEmail
62564
+ });
62565
+ async function ensureUserCache(workspace, cache3) {
62566
+ if (cache3.size > 0)
62567
+ return;
62568
+ const users = await listUsers({ workspace });
62569
+ for (const user of users) {
62570
+ cache3.set(user.username, { username: user.username, email: user.email });
62571
+ cache3.set(user.email, { username: user.username, email: user.email });
62572
+ }
62573
+ }
62574
+ async function lookupUsernameByEmail(workspace, email, cache3) {
62575
+ await ensureUserCache(workspace, cache3);
62576
+ const entry = cache3.get(email);
62577
+ if (!entry) {
62578
+ throw new Error(`Could not find username for email '${email}' in workspace. ` + `Make sure the user exists in the workspace.`);
62579
+ }
62580
+ return entry.username;
62581
+ }
62582
+ function contentHasOnBehalfOf(content, typeStr) {
62583
+ if (typeStr === "script") {
62584
+ return !!content.match(/has_on_behalf_of:\s*(true)/) || !!content.match(/on_behalf_of_email:\s*["']?([^\s"']+)["']?/);
62585
+ }
62586
+ if (typeStr === "flow") {
62587
+ return !!content.match(/has_on_behalf_of:\s*(true)/);
62588
+ }
62589
+ return false;
62590
+ }
62591
+ async function preCheckPermissionedAs(changes, userEmail, userIsAdminOrDeployer, acceptOverride, isInteractive) {
62592
+ if (userIsAdminOrDeployer)
62593
+ return;
62594
+ const wouldChangeItems = [];
62595
+ for (const change of changes) {
62596
+ let typeStr;
62597
+ try {
62598
+ typeStr = getTypeStrFromPath(change.path);
62599
+ } catch {
62600
+ continue;
62601
+ }
62602
+ if (change.name === "added") {
62603
+ const content = change.content;
62604
+ if (!content)
62605
+ continue;
62606
+ const isScriptMeta = typeStr === "script" && (change.path.endsWith(".script.yaml") || change.path.endsWith(".script.json"));
62607
+ const isFlowMeta = typeStr === "flow" && (change.path.endsWith("flow.yaml") || change.path.endsWith("flow.json"));
62608
+ if ((isScriptMeta || isFlowMeta) && contentHasOnBehalfOf(content, typeStr)) {
62609
+ const label = typeStr === "script" ? "(script owner)" : "(flow owner)";
62610
+ wouldChangeItems.push({ path: change.path, currentOwner: label });
62611
+ } else if (typeStr === "app") {
62612
+ wouldChangeItems.push({
62613
+ path: change.path,
62614
+ currentOwner: "(app policy owner)"
62615
+ });
62616
+ }
62617
+ continue;
62618
+ }
62619
+ if (change.name !== "edited")
62620
+ continue;
62621
+ const beforeContent = change.before;
62622
+ if (!beforeContent)
62623
+ continue;
62624
+ let currentOwner;
62625
+ if (typeStr === "script") {
62626
+ if (change.path.endsWith(".script.yaml") || change.path.endsWith(".script.json")) {
62627
+ const hasOboMatch = beforeContent.match(/has_on_behalf_of:\s*(true)/);
62628
+ if (hasOboMatch) {
62629
+ currentOwner = "(script owner)";
62630
+ } else {
62631
+ const emailMatch = beforeContent.match(/on_behalf_of_email:\s*["']?([^\s"']+)["']?/);
62632
+ if (emailMatch) {
62633
+ currentOwner = emailMatch[1];
62634
+ }
62635
+ }
62636
+ }
62637
+ } else if (typeStr === "flow") {
62638
+ if (change.path.endsWith("flow.yaml") || change.path.endsWith("flow.json")) {
62639
+ const hasOboMatch = beforeContent.match(/has_on_behalf_of:\s*(true)/);
62640
+ if (hasOboMatch) {
62641
+ wouldChangeItems.push({
62642
+ path: change.path,
62643
+ currentOwner: "(flow owner)"
62644
+ });
62645
+ }
62646
+ }
62647
+ continue;
62648
+ } else if (typeStr === "app") {
62649
+ wouldChangeItems.push({
62650
+ path: change.path,
62651
+ currentOwner: "(app policy owner)"
62652
+ });
62653
+ continue;
62654
+ } else if (typeStr === "schedule") {
62655
+ const match2 = beforeContent.match(/email:\s*["']?([^\s"']+)["']?/);
62656
+ if (match2) {
62657
+ currentOwner = match2[1];
62658
+ }
62659
+ } else if (typeStr.endsWith("_trigger")) {
62660
+ wouldChangeItems.push({
62661
+ path: change.path,
62662
+ currentOwner: "(trigger owner)"
62663
+ });
62664
+ continue;
62665
+ }
62666
+ if (currentOwner && currentOwner !== userEmail) {
62667
+ wouldChangeItems.push({ path: change.path, currentOwner });
62668
+ }
62669
+ }
62670
+ if (wouldChangeItems.length === 0)
62671
+ return;
62672
+ const itemList = wouldChangeItems.map((item) => ` - ${item.path} (current owner: ${item.currentOwner})`).join(`
62673
+ `);
62674
+ const message = `You are not an admin or member of 'wm_deployers'. The following ${wouldChangeItems.length} item(s) ` + `will have their permissioned_as/email changed to your user (${userEmail}):
62675
+ ${itemList}`;
62676
+ if (acceptOverride) {
62677
+ warn(colors.yellow(`Warning: ${message}`));
62678
+ return;
62679
+ }
62680
+ if (isInteractive) {
62681
+ warn(colors.yellow(message));
62682
+ const proceed = await Confirm.prompt({
62683
+ message: "Do you want to proceed? (use --accept-overriding-permissioned-as-with-self to skip this prompt)",
62684
+ default: false
62685
+ });
62686
+ if (!proceed) {
62687
+ info("Push cancelled.");
62688
+ process.exit(0);
62689
+ }
62690
+ } else {
62691
+ error(colors.red(`${message}
62692
+
62693
+ Use --accept-overriding-permissioned-as-with-self to proceed anyway.`));
62694
+ process.exit(1);
62695
+ }
62696
+ }
62697
+ var init_permissioned_as = __esm(async () => {
62698
+ init_services_gen();
62699
+ init_log();
62700
+ init_colors2();
62701
+ await __promiseAll([
62702
+ init_confirm(),
62703
+ init_types()
62704
+ ]);
62705
+ });
62706
+
62423
62707
  // src/commands/resource/resource.ts
62424
62708
  import { mkdir as mkdir4, stat as stat6, writeFile as writeFile6, readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
62425
62709
  import nodePath from "node:path";
@@ -63652,7 +63936,7 @@ async function findFilesetResourceFile(changePath) {
63652
63936
  }
63653
63937
  throw new Error(`No resource metadata file found for fileset resource: ${changePath}`);
63654
63938
  }
63655
- function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, resourceTypeToIsFileset, ignoreCodebaseChanges) {
63939
+ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, resourceTypeToIsFileset, ignoreCodebaseChanges, stripOnBehalfOf) {
63656
63940
  let _moduleScriptPaths = null;
63657
63941
  async function getModuleScriptPaths() {
63658
63942
  if (_moduleScriptPaths === null) {
@@ -63739,6 +64023,10 @@ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, re
63739
64023
  }
63740
64024
  };
63741
64025
  }
64026
+ if (stripOnBehalfOf) {
64027
+ flow.has_on_behalf_of = !!flow.on_behalf_of_email;
64028
+ delete flow.on_behalf_of_email;
64029
+ }
63742
64030
  yield {
63743
64031
  isDirectory: false,
63744
64032
  path: path9.join(finalPath, "flow.yaml"),
@@ -63942,6 +64230,10 @@ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, re
63942
64230
  if (ignoreCodebaseChanges && parsed["codebase"]) {
63943
64231
  parsed["codebase"] = undefined;
63944
64232
  }
64233
+ if (stripOnBehalfOf) {
64234
+ parsed["has_on_behalf_of"] = !!parsed["on_behalf_of_email"];
64235
+ delete parsed["on_behalf_of_email"];
64236
+ }
63945
64237
  delete parsed["modules"];
63946
64238
  return useYaml ? import_yaml11.stringify(parsed, yamlOptions) : JSON.stringify(parsed, null, 2);
63947
64239
  }
@@ -63964,14 +64256,30 @@ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, re
63964
64256
  }
63965
64257
  return useYaml ? import_yaml11.stringify(parsed, yamlOptions) : JSON.stringify(parsed, null, 2);
63966
64258
  }
63967
- return useYaml && isJson && kind != "dependencies" ? (() => {
64259
+ if (isJson && kind != "dependencies") {
63968
64260
  try {
63969
- return import_yaml11.stringify(JSON.parse(content), yamlOptions);
64261
+ const parsed = JSON.parse(content);
64262
+ if (stripOnBehalfOf) {
64263
+ const isSchedule = p.endsWith(".schedule.json");
64264
+ const isTrigger = p.endsWith("_trigger.json");
64265
+ if (isSchedule) {
64266
+ parsed["has_permissioned_as"] = !!parsed["permissioned_as"];
64267
+ delete parsed["permissioned_as"];
64268
+ delete parsed["email"];
64269
+ delete parsed["edited_by"];
64270
+ } else if (isTrigger) {
64271
+ parsed["has_permissioned_as"] = !!parsed["permissioned_as"];
64272
+ delete parsed["permissioned_as"];
64273
+ delete parsed["edited_by"];
64274
+ }
64275
+ }
64276
+ return useYaml ? import_yaml11.stringify(parsed, yamlOptions) : JSON.stringify(parsed, null, 2);
63970
64277
  } catch (error2) {
63971
64278
  error(`Failed to parse JSON content at path: ${p}`);
63972
64279
  throw error2;
63973
64280
  }
63974
- })() : content;
64281
+ }
64282
+ return content;
63975
64283
  }
63976
64284
  }
63977
64285
  ];
@@ -64692,7 +65000,7 @@ async function pull(opts) {
64692
65000
  resourceTypeToIsFileset = parsed.filesetMap;
64693
65001
  } catch {}
64694
65002
  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);
64695
- const remote = ZipFSElement(zipFile, !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, resourceTypeToIsFileset, true);
65003
+ const remote = ZipFSElement(zipFile, !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, resourceTypeToIsFileset, true, parseSyncBehavior(opts.syncBehavior) >= 1);
64696
65004
  const local = !opts.stateful ? await FSFSElement(process.cwd(), codebases, true) : await FSFSElement(path9.join(process.cwd(), ".wmill"), [], true);
64697
65005
  const changes = await compareDynFSElement(remote, local, await ignoreF(opts), opts.json ?? false, opts, false, codebases, true, specificItems, wsNameForFiles, true);
64698
65006
  info(`remote (${workspace.name}) -> local: ${changes.length} changes to apply`);
@@ -64859,7 +65167,7 @@ Done! All ${changes.length} changes applied locally and wmill-lock.yaml updated.
64859
65167
  console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
64860
65168
  }
64861
65169
  }
64862
- function prettyChanges(changes, specificItems, branchOverride) {
65170
+ function prettyChanges(changes, specificItems, branchOverride, folderDefaultAnnotations) {
64863
65171
  for (const change of changes) {
64864
65172
  let displayPath = change.path;
64865
65173
  let wsNote = "";
@@ -64870,8 +65178,10 @@ function prettyChanges(changes, specificItems, branchOverride) {
64870
65178
  wsNote = " (workspace-specific)";
64871
65179
  }
64872
65180
  }
65181
+ const folderNote = folderDefaultAnnotations?.get(change.path);
65182
+ const extraNote = folderNote ? colors.cyan(` (will be permissioned as ${folderNote} via folder default)`) : "";
64873
65183
  if (change.name === "added") {
64874
- info(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + displayPath + colors.gray(wsNote)));
65184
+ info(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + displayPath + colors.gray(wsNote)) + extraNote);
64875
65185
  } else if (change.name === "deleted") {
64876
65186
  info(colors.red(`- ${getTypeStrFromPath(change.path)} ` + displayPath + colors.gray(wsNote)));
64877
65187
  } else if (change.name === "edited") {
@@ -64979,7 +65289,7 @@ Push aborted: ${lockIssues.length} script(s) missing locks.`));
64979
65289
  resourceTypeToFormatExtension = parsed.formatExtMap;
64980
65290
  resourceTypeToIsFileset = parsed.filesetMap;
64981
65291
  } catch {}
64982
- const remote = ZipFSElement(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), !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, resourceTypeToIsFileset, false);
65292
+ const remote = ZipFSElement(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), !opts.json, opts.defaultTs ?? "bun", resourceTypeToFormatExtension, resourceTypeToIsFileset, false, parseSyncBehavior(opts.syncBehavior) >= 1);
64983
65293
  const local = await FSFSElement(path9.join(process.cwd(), ""), codebases, false);
64984
65294
  const changes = await compareDynFSElement(local, remote, await ignoreF(opts), opts.json ?? false, opts, true, codebases, false, specificItems, wsNameForFiles, false);
64985
65295
  const rawWorkspaceDependencies = await getRawWorkspaceDependencies(true);
@@ -65152,13 +65462,59 @@ ${folderList}
65152
65462
  return;
65153
65463
  }
65154
65464
  if (changes.length > 0) {
65465
+ let folderDefaultAnnotations;
65466
+ if (parseSyncBehavior(opts.syncBehavior) >= 1) {
65467
+ folderDefaultAnnotations = new Map;
65468
+ const folderRulesCache = new Map;
65469
+ for (const change of changes) {
65470
+ if (change.name !== "added")
65471
+ continue;
65472
+ const match2 = change.path.match(/^f\/([^/]+)\//);
65473
+ if (!match2)
65474
+ continue;
65475
+ const folderName2 = match2[1];
65476
+ if (!folderRulesCache.has(folderName2)) {
65477
+ try {
65478
+ const folder = await getFolder({ workspace: workspace.workspaceId, name: folderName2 });
65479
+ folderRulesCache.set(folderName2, folder.default_permissioned_as ?? []);
65480
+ } catch {
65481
+ folderRulesCache.set(folderName2, []);
65482
+ }
65483
+ }
65484
+ const rules = folderRulesCache.get(folderName2);
65485
+ const remotePath = change.path.replace(/\.(script|schedule|http_trigger|websocket_trigger|kafka_trigger|nats_trigger|postgres_trigger|mqtt_trigger|sqs_trigger|gcp_trigger|email_trigger)\.(yaml|json)$/, "").replace(/(\.flow|__flow)\/flow\.(yaml|json)$/, "").replace(/\.(app|raw_app)(\/app\.(yaml|json))?$/, "");
65486
+ const relative6 = remotePath.slice(`f/${folderName2}/`.length);
65487
+ if (!relative6)
65488
+ continue;
65489
+ for (const rule of rules) {
65490
+ if (minimatch(relative6, rule.path_glob)) {
65491
+ folderDefaultAnnotations.set(change.path, rule.permissioned_as);
65492
+ break;
65493
+ }
65494
+ }
65495
+ }
65496
+ }
65155
65497
  if (!opts.jsonOutput) {
65156
- prettyChanges(changes, specificItems, wsNameForFiles);
65498
+ prettyChanges(changes, specificItems, wsNameForFiles, folderDefaultAnnotations);
65157
65499
  }
65158
65500
  if (opts.dryRun) {
65159
65501
  info(colors.gray(`Dry run complete.`));
65160
65502
  return;
65161
65503
  }
65504
+ let permissionedAsContext = undefined;
65505
+ if (parseSyncBehavior(opts.syncBehavior) >= 1) {
65506
+ const user = await whoami({ workspace: workspace.workspaceId });
65507
+ const userIsAdminOrDeployer = user.is_admin || (user.groups ?? []).includes("wm_deployers");
65508
+ debug(`permissioned_as: user=${user.email}, is_admin=${user.is_admin}, groups=${JSON.stringify(user.groups)}, isAdminOrDeployer=${userIsAdminOrDeployer}`);
65509
+ permissionedAsContext = {
65510
+ userCache: new Map,
65511
+ userIsAdminOrDeployer,
65512
+ userEmail: user.email
65513
+ };
65514
+ await preCheckPermissionedAs(changes, user.email, userIsAdminOrDeployer, opts.acceptOverridingPermissionedAsWithSelf ?? false, !!process.stdin.isTTY);
65515
+ } else if (folderDefaultAnnotations && folderDefaultAnnotations.size > 0) {
65516
+ warn(colors.yellow(`This workspace has folder default_permissioned_as rules that affect ${folderDefaultAnnotations.size} item(s) being pushed, ` + `but syncBehavior is not set in wmill.yaml. Add 'syncBehavior: v1' to enable ownership preservation on update and on_behalf_of stripping on pull.`));
65517
+ }
65162
65518
  if (!opts.yes && !await Confirm.prompt({
65163
65519
  message: `Do you want to apply these ${changes.length} changes to the remote?`,
65164
65520
  default: true
@@ -65187,17 +65543,24 @@ ${folderList}
65187
65543
  if (parallelizationFactor <= 0) {
65188
65544
  parallelizationFactor = 1;
65189
65545
  }
65190
- const groupedChangesArray = Array.from(groupedChanges.entries());
65191
- info(`found changes for ${groupedChangesArray.length} items with a total of ${groupedChangesArray.reduce((acc, [_, changes2]) => acc + changes2.length, 0)} files to process`);
65546
+ const allGrouped = Array.from(groupedChanges.entries());
65547
+ const isFolderMetaGroup = ([basePath]) => basePath.endsWith(`${SEP8}folder`) || basePath === "folder";
65548
+ const folderMetaGroups = allGrouped.filter(isFolderMetaGroup);
65549
+ const groupedChangesArray = allGrouped.filter((g) => !isFolderMetaGroup(g));
65550
+ info(`found changes for ${allGrouped.length} items with a total of ${allGrouped.reduce((acc, [_, changes2]) => acc + changes2.length, 0)} files to process`);
65192
65551
  if (parallelizationFactor > 1) {
65193
65552
  info(`Parallelizing ${parallelizationFactor} changes at a time`);
65194
65553
  }
65195
65554
  const pool = new Set;
65196
- const queue = [...groupedChangesArray];
65555
+ const queue = [...folderMetaGroups, ...groupedChangesArray];
65556
+ let folderPhaseRemaining = folderMetaGroups.length;
65557
+ const effectiveParallelism = () => folderPhaseRemaining > 0 ? 1 : parallelizationFactor;
65197
65558
  const cachedWsNameForPush = wsNameForFiles || (isGitRepository() ? getCurrentGitBranch() : null);
65198
65559
  while (queue.length > 0 || pool.size > 0) {
65199
- while (pool.size < parallelizationFactor && queue.length > 0) {
65200
- let [_basePath, changes2] = queue.shift();
65560
+ while (pool.size < effectiveParallelism() && queue.length > 0) {
65561
+ const [groupBasePath, initialChanges] = queue.shift();
65562
+ let changes2 = initialChanges;
65563
+ const isFolderGroup = groupBasePath.endsWith(`${SEP8}folder`) || groupBasePath === "folder";
65201
65564
  const promise = (async () => {
65202
65565
  const alreadySynced = [];
65203
65566
  const deletedVarsResPaths = [];
@@ -65221,12 +65584,12 @@ ${folderList}
65221
65584
  }
65222
65585
  }
65223
65586
  if (change.name === "edited") {
65224
- if (await handleScriptMetadata(change.path, workspace, alreadySynced, opts.message, rawWorkspaceDependencies, codebases, opts)) {
65587
+ if (await handleScriptMetadata(change.path, workspace, alreadySynced, opts.message, rawWorkspaceDependencies, codebases, opts, permissionedAsContext)) {
65225
65588
  if (stateTarget) {
65226
65589
  await writeFile7(stateTarget, change.after, "utf-8");
65227
65590
  }
65228
65591
  continue;
65229
- } else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, rawWorkspaceDependencies, codebases)) {
65592
+ } else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, rawWorkspaceDependencies, codebases, permissionedAsContext)) {
65230
65593
  if (stateTarget) {
65231
65594
  await writeFile7(stateTarget, change.after, "utf-8");
65232
65595
  }
@@ -65282,14 +65645,14 @@ ${folderList}
65282
65645
  if (specificItems && isSpecificItem(change.path, specificItems)) {
65283
65646
  originalWorkspaceSpecificPath = getWorkspaceSpecificPath(change.path, specificItems, wsNameForFiles);
65284
65647
  }
65285
- await pushObj(workspace.workspaceId, change.path, oldObj, newObj, opts.plainSecrets ?? false, alreadySynced, opts.message, originalWorkspaceSpecificPath);
65648
+ await pushObj(workspace.workspaceId, change.path, oldObj, newObj, opts.plainSecrets ?? false, alreadySynced, opts.message, originalWorkspaceSpecificPath, permissionedAsContext);
65286
65649
  if (stateTarget) {
65287
65650
  await writeFile7(stateTarget, change.after, "utf-8");
65288
65651
  }
65289
65652
  } else if (change.name === "added") {
65290
65653
  if (change.path.endsWith(".script.json") || change.path.endsWith(".script.yaml") || change.path.endsWith(".lock") || isFileResource(change.path) || isFilesetResource(change.path)) {
65291
65654
  continue;
65292
- } else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, rawWorkspaceDependencies, codebases)) {
65655
+ } else if (await handleFile(change.path, workspace, alreadySynced, opts.message, opts, rawWorkspaceDependencies, codebases, permissionedAsContext)) {
65293
65656
  continue;
65294
65657
  } else if (isScriptModulePath(change.path)) {
65295
65658
  await pushParentScriptForModule(change.path, workspace, alreadySynced, opts.message, opts, rawWorkspaceDependencies, codebases);
@@ -65307,7 +65670,7 @@ ${folderList}
65307
65670
  localFilePath = workspaceSpecificPath;
65308
65671
  }
65309
65672
  }
65310
- await pushObj(workspace.workspaceId, change.path, undefined, obj, opts.plainSecrets ?? false, [], opts.message, localFilePath);
65673
+ await pushObj(workspace.workspaceId, change.path, undefined, obj, opts.plainSecrets ?? false, [], opts.message, localFilePath, permissionedAsContext);
65311
65674
  if (stateTarget) {
65312
65675
  await writeFile7(stateTarget, change.content, "utf-8");
65313
65676
  }
@@ -65583,7 +65946,11 @@ ${folderList}
65583
65946
  }
65584
65947
  })();
65585
65948
  pool.add(promise);
65586
- promise.then(() => pool.delete(promise));
65949
+ promise.then(() => {
65950
+ pool.delete(promise);
65951
+ if (isFolderGroup)
65952
+ folderPhaseRemaining--;
65953
+ });
65587
65954
  }
65588
65955
  if (pool.size > 0) {
65589
65956
  await Promise.race(pool);
@@ -65659,6 +66026,7 @@ var init_sync = __esm(async () => {
65659
66026
  init_script(),
65660
66027
  init_utils(),
65661
66028
  init_conf(),
66029
+ init_permissioned_as(),
65662
66030
  init_specific_items(),
65663
66031
  init_types(),
65664
66032
  init_codebase(),
@@ -65676,7 +66044,7 @@ var init_sync = __esm(async () => {
65676
66044
  aliasDuplicateObjects: false,
65677
66045
  singleQuote: true
65678
66046
  };
65679
- command7 = new Command().description("sync local with a remote workspaces or the opposite (push or pull)").action(() => info("2 actions available, pull and push. Use -h to display help.")).command("pull").description("Pull any remote changes and apply them locally.").option("--yes", "Pull without needing confirmation").option("--dry-run", "Show changes that would be pulled without actually pushing").option("--plain-secrets", "Pull secrets as plain text").option("--json", "Use JSON instead of YAML").option("--skip-variables", "Skip syncing variables (including secrets)").option("--skip-secrets", "Skip syncing only secrets variables").option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)").option("--skip-resources", "Skip syncing resources").option("--skip-resource-types", "Skip syncing resource types").option("--skip-scripts", "Skip syncing scripts").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("--include-schedules", "Include syncing schedules").option("--include-triggers", "Include syncing triggers").option("--include-users", "Include syncing users").option("--include-groups", "Include syncing groups").option("--include-settings", "Include syncing workspace settings").option("--include-key", "Include workspace encryption key").option("--skip-branch-validation", "Skip git branch validation and prompts").option("--json-output", "Output results in JSON format").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account. Overrides wmill.yaml excludes").option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy").option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist").option("--promotion <branch:string>", "Use promotionOverrides from the specified branch instead of regular overrides").option("--branch, --env <branch:string>", "[Deprecated: use --workspace] Override the current git branch/environment").action(pull).command("push").description("Push any local changes and apply them remotely.").option("--yes", "Push without needing confirmation").option("--dry-run", "Show changes that would be pushed without actually pushing").option("--plain-secrets", "Push secrets as plain text").option("--json", "Use JSON instead of YAML").option("--skip-variables", "Skip syncing variables (including secrets)").option("--skip-secrets", "Skip syncing only secrets variables").option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)").option("--skip-resources", "Skip syncing resources").option("--skip-resource-types", "Skip syncing resource types").option("--skip-scripts", "Skip syncing scripts").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("--include-schedules", "Include syncing schedules").option("--include-triggers", "Include syncing triggers").option("--include-users", "Include syncing users").option("--include-groups", "Include syncing groups").option("--include-settings", "Include syncing workspace settings").option("--include-key", "Include workspace encryption key").option("--skip-branch-validation", "Skip git branch validation and prompts").option("--json-output", "Output results in JSON format").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy").option("--message <message:string>", "Include a message that will be added to all scripts/flows/apps updated during this push").option("--parallel <number>", "Number of changes to process in parallel").option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist").option("--branch, --env <branch:string>", "[Deprecated: use --workspace] Override the current git branch/environment").option("--lint", "Run lint validation before pushing").option("--locks-required", "Fail if scripts or flow inline scripts that need locks have no locks").option("--auto-metadata", "Automatically regenerate stale metadata (locks and schemas) before pushing").action(push4);
66047
+ command7 = new Command().description("sync local with a remote workspaces or the opposite (push or pull)").action(() => info("2 actions available, pull and push. Use -h to display help.")).command("pull").description("Pull any remote changes and apply them locally.").option("--yes", "Pull without needing confirmation").option("--dry-run", "Show changes that would be pulled without actually pushing").option("--plain-secrets", "Pull secrets as plain text").option("--json", "Use JSON instead of YAML").option("--skip-variables", "Skip syncing variables (including secrets)").option("--skip-secrets", "Skip syncing only secrets variables").option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)").option("--skip-resources", "Skip syncing resources").option("--skip-resource-types", "Skip syncing resource types").option("--skip-scripts", "Skip syncing scripts").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("--include-schedules", "Include syncing schedules").option("--include-triggers", "Include syncing triggers").option("--include-users", "Include syncing users").option("--include-groups", "Include syncing groups").option("--include-settings", "Include syncing workspace settings").option("--include-key", "Include workspace encryption key").option("--skip-branch-validation", "Skip git branch validation and prompts").option("--json-output", "Output results in JSON format").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account. Overrides wmill.yaml excludes").option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy").option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist").option("--promotion <branch:string>", "Use promotionOverrides from the specified branch instead of regular overrides").option("--branch, --env <branch:string>", "[Deprecated: use --workspace] Override the current git branch/environment").action(pull).command("push").description("Push any local changes and apply them remotely.").option("--yes", "Push without needing confirmation").option("--dry-run", "Show changes that would be pushed without actually pushing").option("--plain-secrets", "Push secrets as plain text").option("--json", "Use JSON instead of YAML").option("--skip-variables", "Skip syncing variables (including secrets)").option("--skip-secrets", "Skip syncing only secrets variables").option("--include-secrets", "Include secrets in sync (overrides skipSecrets in wmill.yaml)").option("--skip-resources", "Skip syncing resources").option("--skip-resource-types", "Skip syncing resource types").option("--skip-scripts", "Skip syncing scripts").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("--include-schedules", "Include syncing schedules").option("--include-triggers", "Include syncing triggers").option("--include-users", "Include syncing users").option("--include-groups", "Include syncing groups").option("--include-settings", "Include syncing workspace settings").option("--include-key", "Include workspace encryption key").option("--skip-branch-validation", "Skip git branch validation and prompts").option("--json-output", "Output results in JSON format").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").option("--extra-includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy").option("--message <message:string>", "Include a message that will be added to all scripts/flows/apps updated during this push").option("--parallel <number>", "Number of changes to process in parallel").option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo) when multiple repositories exist").option("--branch, --env <branch:string>", "[Deprecated: use --workspace] Override the current git branch/environment").option("--lint", "Run lint validation before pushing").option("--locks-required", "Fail if scripts or flow inline scripts that need locks have no locks").option("--auto-metadata", "Automatically regenerate stale metadata (locks and schemas) before pushing").option("--accept-overriding-permissioned-as-with-self", "Accept that items with a different permissioned_as will be updated with your own user").action(push4);
65680
66048
  sync_default = command7;
65681
66049
  });
65682
66050
 
@@ -69487,7 +69855,7 @@ function replaceInlineScripts2(rec, localPath, addType) {
69487
69855
  function isExecutionModeAnonymous(app) {
69488
69856
  return app?.["policy"]?.["execution_mode"] == "anonymous";
69489
69857
  }
69490
- async function pushApp(workspace, remotePath, localPath, message) {
69858
+ async function pushApp(workspace, remotePath, localPath, message, permissionedAsContext) {
69491
69859
  if (alreadySynced2.includes(localPath)) {
69492
69860
  return;
69493
69861
  }
@@ -69500,6 +69868,12 @@ async function pushApp(workspace, remotePath, localPath, message) {
69500
69868
  path: remotePath
69501
69869
  });
69502
69870
  } catch {}
69871
+ let remoteOnBehalfOf;
69872
+ let remoteOnBehalfOfEmail;
69873
+ if (app?.policy) {
69874
+ remoteOnBehalfOf = app.policy.on_behalf_of;
69875
+ remoteOnBehalfOfEmail = app.policy.on_behalf_of_email;
69876
+ }
69503
69877
  if (isExecutionModeAnonymous(app)) {
69504
69878
  app.public = true;
69505
69879
  }
@@ -69513,6 +69887,17 @@ async function pushApp(workspace, remotePath, localPath, message) {
69513
69887
  const localApp = await yamlParseFile(path17);
69514
69888
  replaceInlineScripts2(localApp.value, localPath, true);
69515
69889
  await generatingPolicy2(localApp, remotePath, localApp?.["public"] ?? localApp?.["policy"]?.["execution_mode"] == "anonymous");
69890
+ const preserveFields = {};
69891
+ if (permissionedAsContext?.userIsAdminOrDeployer) {
69892
+ if (app) {
69893
+ if (localApp.policy && remoteOnBehalfOf) {
69894
+ localApp.policy.on_behalf_of = remoteOnBehalfOf;
69895
+ localApp.policy.on_behalf_of_email = remoteOnBehalfOfEmail;
69896
+ preserveFields.preserve_on_behalf_of = true;
69897
+ info(`Preserving ${remoteOnBehalfOfEmail ?? remoteOnBehalfOf} as permissioned_as for app ${remotePath}`);
69898
+ }
69899
+ }
69900
+ }
69516
69901
  if (app) {
69517
69902
  if (isSuperset(localApp, app)) {
69518
69903
  info(colors.green(`App ${remotePath} is up to date`));
@@ -69524,7 +69909,8 @@ async function pushApp(workspace, remotePath, localPath, message) {
69524
69909
  path: remotePath,
69525
69910
  requestBody: {
69526
69911
  deployment_message: message,
69527
- ...localApp
69912
+ ...localApp,
69913
+ ...preserveFields
69528
69914
  }
69529
69915
  });
69530
69916
  } else {
@@ -69534,7 +69920,8 @@ async function pushApp(workspace, remotePath, localPath, message) {
69534
69920
  requestBody: {
69535
69921
  path: remotePath,
69536
69922
  deployment_message: message,
69537
- ...localApp
69923
+ ...localApp,
69924
+ ...preserveFields
69538
69925
  }
69539
69926
  });
69540
69927
  }
@@ -69639,6 +70026,31 @@ var init_app = __esm(async () => {
69639
70026
  warn(colors.yellow('This command is deprecated. Use "wmill generate-metadata" instead.'));
69640
70027
  const { generateLocksCommand: generateLocksCommand2 } = await init_app_metadata().then(() => exports_app_metadata);
69641
70028
  await generateLocksCommand2(opts, appFolder);
70029
+ }).command("set-permissioned-as", "Set the on_behalf_of_email for an app (requires admin or wm_deployers group)").arguments("<path:string> <email:string>").action(async (opts, appPath, email) => {
70030
+ const workspace = await resolveWorkspace(opts);
70031
+ await requireLogin(opts);
70032
+ const { lookupUsernameByEmail: lookupUsernameByEmail2 } = await init_permissioned_as().then(() => exports_permissioned_as);
70033
+ const cache3 = new Map;
70034
+ const username = await lookupUsernameByEmail2(workspace.workspaceId, email, cache3);
70035
+ const remote = await getAppByPath({
70036
+ workspace: workspace.workspaceId,
70037
+ path: appPath
70038
+ });
70039
+ if (!remote)
70040
+ throw new Error(`App ${appPath} not found`);
70041
+ await updateApp({
70042
+ workspace: workspace.workspaceId,
70043
+ path: appPath,
70044
+ requestBody: {
70045
+ policy: {
70046
+ ...remote.policy,
70047
+ on_behalf_of: `u/${username}`,
70048
+ on_behalf_of_email: email
70049
+ },
70050
+ preserve_on_behalf_of: true
70051
+ }
70052
+ });
70053
+ info(colors.green(`Updated permissioned_as for app ${appPath} to ${email}`));
69642
70054
  });
69643
70055
  app_default = command12;
69644
70056
  });
@@ -69818,7 +70230,54 @@ var init_folder = __esm(async () => {
69818
70230
  init_types()
69819
70231
  ]);
69820
70232
  import_yaml24 = __toESM(require_dist(), 1);
69821
- command13 = new Command().description("folder related commands").option("--json", "Output as JSON (for piping to jq)").action(list7).command("list", "list all folders").option("--json", "Output as JSON (for piping to jq)").action(list7).command("get", "get a folder's details").arguments("<name:string>").option("--json", "Output as JSON (for piping to jq)").action(get5).command("new", "create a new folder locally").arguments("<name:string>").option("--summary <summary:string>", "folder summary").action(newFolder).command("push", "push a local folder to the remote by name. This overrides any remote versions.").arguments("<name:string>").action(push6).command("add-missing", "create default folder.meta.yaml for all subdirectories of f/ that are missing one").option("-y, --yes", "skip confirmation prompt").action(addMissing);
70233
+ command13 = new Command().description("folder related commands").option("--json", "Output as JSON (for piping to jq)").action(list7).command("list", "list all folders").option("--json", "Output as JSON (for piping to jq)").action(list7).command("get", "get a folder's details").arguments("<name:string>").option("--json", "Output as JSON (for piping to jq)").action(get5).command("new", "create a new folder locally").arguments("<name:string>").option("--summary <summary:string>", "folder summary").action(newFolder).command("push", "push a local folder to the remote by name. This overrides any remote versions.").arguments("<name:string>").action(push6).command("add-missing", "create default folder.meta.yaml for all subdirectories of f/ that are missing one").option("-y, --yes", "skip confirmation prompt").action(addMissing).command("show-rules", "Show default_permissioned_as rules for a folder. Use --test-path to see which rule matches a given item path.").arguments("<name:string>").option("--test-path <path:string>", "Test which rule matches this item path (e.g. f/prod/jobs/my_script)").option("--json", "Output as JSON").action(async (opts, folderName2) => {
70234
+ const workspace = await resolveWorkspace(opts);
70235
+ await requireLogin(opts);
70236
+ const folder = await getFolder({
70237
+ workspace: workspace.workspaceId,
70238
+ name: folderName2
70239
+ });
70240
+ const rules = folder.default_permissioned_as ?? [];
70241
+ if (opts.json && !opts.testPath) {
70242
+ console.log(JSON.stringify(rules, null, 2));
70243
+ return;
70244
+ }
70245
+ if (rules.length === 0) {
70246
+ info(`Folder '${folderName2}' has no default_permissioned_as rules.`);
70247
+ return;
70248
+ }
70249
+ if (!opts.testPath) {
70250
+ info(colors.bold(`Rules for folder '${folderName2}' (first match wins):
70251
+ `));
70252
+ new Table2().header(["#", "path_glob", "permissioned_as"]).padding(2).border(true).body(rules.map((r, i) => [String(i + 1), r.path_glob, r.permissioned_as])).render();
70253
+ return;
70254
+ }
70255
+ const testPath = opts.testPath;
70256
+ const prefix = `f/${folderName2}/`;
70257
+ if (!testPath.startsWith(prefix)) {
70258
+ error(`Path '${testPath}' is not under folder '${folderName2}' (expected prefix '${prefix}')`);
70259
+ return;
70260
+ }
70261
+ const relative7 = testPath.slice(prefix.length);
70262
+ const { minimatch: minimatch2 } = await Promise.resolve().then(() => (init_esm2(), exports_esm));
70263
+ for (let i = 0;i < rules.length; i++) {
70264
+ const rule = rules[i];
70265
+ if (minimatch2(relative7, rule.path_glob)) {
70266
+ if (opts.json) {
70267
+ console.log(JSON.stringify({ matched: true, rule_index: i, rule, relative_path: relative7 }));
70268
+ } else {
70269
+ info(colors.green(`✓ Rule #${i + 1} matches: path_glob='${rule.path_glob}' → permissioned_as='${rule.permissioned_as}'`));
70270
+ info(colors.gray(` (relative path tested: '${relative7}')`));
70271
+ }
70272
+ return;
70273
+ }
70274
+ }
70275
+ if (opts.json) {
70276
+ console.log(JSON.stringify({ matched: false, relative_path: relative7 }));
70277
+ } else {
70278
+ info(colors.yellow(`No rule matches path '${testPath}' (relative: '${relative7}')`));
70279
+ }
70280
+ });
69822
70281
  folder_default = command13;
69823
70282
  });
69824
70283
 
@@ -70053,7 +70512,7 @@ async function get7(opts, path17) {
70053
70512
  console.log(colors.bold("Enabled:") + " " + (s.enabled ? "true" : "false"));
70054
70513
  }
70055
70514
  }
70056
- async function pushSchedule(workspace, path17, schedule, localSchedule) {
70515
+ async function pushSchedule(workspace, path17, schedule, localSchedule, permissionedAsContext) {
70057
70516
  path17 = removeType(path17, "schedule").replaceAll(SEP16, "/");
70058
70517
  debug(`Processing local schedule ${path17}`);
70059
70518
  try {
@@ -70062,6 +70521,17 @@ async function pushSchedule(workspace, path17, schedule, localSchedule) {
70062
70521
  } catch {
70063
70522
  debug(`Schedule ${path17} does not exist on remote`);
70064
70523
  }
70524
+ delete localSchedule.has_permissioned_as;
70525
+ const preserveFields = {};
70526
+ if (permissionedAsContext?.userIsAdminOrDeployer) {
70527
+ if (schedule) {
70528
+ preserveFields.preserve_permissioned_as = true;
70529
+ if (schedule.permissioned_as) {
70530
+ preserveFields.permissioned_as = schedule.permissioned_as;
70531
+ info(`Preserving ${schedule.permissioned_as} as permissioned_as for schedule ${path17}`);
70532
+ }
70533
+ }
70534
+ }
70065
70535
  if (schedule) {
70066
70536
  if (isSuperset(localSchedule, schedule)) {
70067
70537
  debug(`Schedule ${path17} is up to date`);
@@ -70074,7 +70544,8 @@ async function pushSchedule(workspace, path17, schedule, localSchedule) {
70074
70544
  workspace,
70075
70545
  path: path17,
70076
70546
  requestBody: {
70077
- ...localSchedule
70547
+ ...localSchedule,
70548
+ ...preserveFields
70078
70549
  }
70079
70550
  });
70080
70551
  if (localSchedule.enabled != schedule.enabled) {
@@ -70098,7 +70569,8 @@ async function pushSchedule(workspace, path17, schedule, localSchedule) {
70098
70569
  workspace,
70099
70570
  requestBody: {
70100
70571
  path: path17,
70101
- ...localSchedule
70572
+ ...localSchedule,
70573
+ ...preserveFields
70102
70574
  }
70103
70575
  });
70104
70576
  } catch (e) {
@@ -70154,10 +70626,32 @@ var init_schedule = __esm(async () => {
70154
70626
  init_auth(),
70155
70627
  init_context(),
70156
70628
  init_conf(),
70629
+ init_permissioned_as(),
70157
70630
  init_types()
70158
70631
  ]);
70159
70632
  import_yaml26 = __toESM(require_dist(), 1);
70160
- command15 = new Command().description("schedule related commands").option("--json", "Output as JSON (for piping to jq)").action(list9).command("list", "list all schedules").option("--json", "Output as JSON (for piping to jq)").action(list9).command("get", "get a schedule's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(get7).command("new", "create a new schedule locally").arguments("<path:string>").action(newSchedule).command("push", "push a local schedule spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").action(push8).command("enable", "Enable a schedule").arguments("<path:string>").action(enable).command("disable", "Disable a schedule").arguments("<path:string>").action(disable);
70633
+ command15 = new Command().description("schedule related commands").option("--json", "Output as JSON (for piping to jq)").action(list9).command("list", "list all schedules").option("--json", "Output as JSON (for piping to jq)").action(list9).command("get", "get a schedule's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(get7).command("new", "create a new schedule locally").arguments("<path:string>").action(newSchedule).command("push", "push a local schedule spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").action(push8).command("enable", "Enable a schedule").arguments("<path:string>").action(enable).command("disable", "Disable a schedule").arguments("<path:string>").action(disable).command("set-permissioned-as", "Set the email (run-as user) for a schedule (requires admin or wm_deployers group)").arguments("<path:string> <email:string>").action(async (opts, schedulePath, email) => {
70634
+ const workspace = await resolveWorkspace(opts);
70635
+ await requireLogin(opts);
70636
+ const cache3 = new Map;
70637
+ const username = await lookupUsernameByEmail(workspace.workspaceId, email, cache3);
70638
+ const remote = await getSchedule({
70639
+ workspace: workspace.workspaceId,
70640
+ path: schedulePath
70641
+ });
70642
+ if (!remote)
70643
+ throw new Error(`Schedule ${schedulePath} not found`);
70644
+ await updateSchedule({
70645
+ workspace: workspace.workspaceId,
70646
+ path: schedulePath,
70647
+ requestBody: {
70648
+ ...remote,
70649
+ permissioned_as: `u/${username}`,
70650
+ preserve_permissioned_as: true
70651
+ }
70652
+ });
70653
+ info(colors.green(`Updated permissioned_as for schedule ${schedulePath} to ${email} (username: ${username})`));
70654
+ });
70161
70655
  schedule_default = command15;
70162
70656
  });
70163
70657
 
@@ -71729,7 +72223,7 @@ async function createTrigger(triggerType, workspace, path18, trigger) {
71729
72223
  const triggerFunction = triggerFunctions[triggerType];
71730
72224
  await triggerFunction({ workspace, path: path18, requestBody: trigger });
71731
72225
  }
71732
- async function pushTrigger(triggerType, workspace, path18, trigger, localTrigger) {
72226
+ async function pushTrigger(triggerType, workspace, path18, trigger, localTrigger, permissionedAsContext) {
71733
72227
  path18 = removeType(path18, triggerType + "_trigger").replaceAll(SEP17, "/");
71734
72228
  debug(`Processing local ${triggerType} trigger ${path18}`);
71735
72229
  try {
@@ -71738,6 +72232,17 @@ async function pushTrigger(triggerType, workspace, path18, trigger, localTrigger
71738
72232
  } catch {
71739
72233
  debug(`${triggerType} trigger ${path18} does not exist on remote`);
71740
72234
  }
72235
+ delete localTrigger.has_permissioned_as;
72236
+ const preserveFields = {};
72237
+ if (permissionedAsContext?.userIsAdminOrDeployer) {
72238
+ if (trigger) {
72239
+ preserveFields.preserve_permissioned_as = true;
72240
+ if (trigger.permissioned_as) {
72241
+ preserveFields.permissioned_as = trigger.permissioned_as;
72242
+ info(`Preserving ${trigger.permissioned_as} as permissioned_as for trigger ${path18}`);
72243
+ }
72244
+ }
72245
+ }
71741
72246
  if (trigger) {
71742
72247
  if (isSuperset(localTrigger, trigger)) {
71743
72248
  debug(`${triggerType} trigger ${path18} is up to date`);
@@ -71747,6 +72252,7 @@ async function pushTrigger(triggerType, workspace, path18, trigger, localTrigger
71747
72252
  try {
71748
72253
  await updateTrigger(triggerType, workspace, path18, {
71749
72254
  ...localTrigger,
72255
+ ...preserveFields,
71750
72256
  path: path18
71751
72257
  });
71752
72258
  } catch (e) {
@@ -71758,6 +72264,7 @@ async function pushTrigger(triggerType, workspace, path18, trigger, localTrigger
71758
72264
  try {
71759
72265
  await createTrigger(triggerType, workspace, path18, {
71760
72266
  ...localTrigger,
72267
+ ...preserveFields,
71761
72268
  path: path18
71762
72269
  });
71763
72270
  } catch (e) {
@@ -72116,7 +72623,29 @@ var init_trigger = __esm(async () => {
72116
72623
  }
72117
72624
  };
72118
72625
  TRIGGER_SKIP_FIELDS = new Set(["workspace_id", "extra_perms", "edited_by", "edited_at"]);
72119
- command19 = new Command().description("trigger related commands").option("--json", "Output as JSON (for piping to jq)").action(list11).command("list", "list all triggers").option("--json", "Output as JSON (for piping to jq)").action(list11).command("get", "get a trigger's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").option("--kind <kind:string>", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup").action(get8).command("new", "create a new trigger locally").arguments("<path:string>").option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)").action(newTrigger).command("push", "push a local trigger spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").action(push10);
72626
+ command19 = new Command().description("trigger related commands").option("--json", "Output as JSON (for piping to jq)").action(list11).command("list", "list all triggers").option("--json", "Output as JSON (for piping to jq)").action(list11).command("get", "get a trigger's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").option("--kind <kind:string>", "Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup").action(get8).command("new", "create a new trigger locally").arguments("<path:string>").option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)").action(newTrigger).command("push", "push a local trigger spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").action(push10).command("set-permissioned-as", "Set the email (run-as user) for a trigger (requires admin or wm_deployers group)").arguments("<path:string> <email:string>").option("--kind <kind:string>", "Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)").action(async (opts, triggerPath, email) => {
72627
+ const workspace = await resolveWorkspace(opts);
72628
+ await requireLogin(opts);
72629
+ if (!opts.kind) {
72630
+ throw new Error("--kind is required. Valid kinds: " + TRIGGER_TYPES.join(", "));
72631
+ }
72632
+ if (!checkIfValidTrigger(opts.kind)) {
72633
+ throw new Error("Invalid trigger kind: " + opts.kind + ". Valid kinds: " + TRIGGER_TYPES.join(", "));
72634
+ }
72635
+ const { lookupUsernameByEmail: lookupUsernameByEmail2 } = await init_permissioned_as().then(() => exports_permissioned_as);
72636
+ const cache3 = new Map;
72637
+ const username = await lookupUsernameByEmail2(workspace.workspaceId, email, cache3);
72638
+ const remote = await getTrigger(opts.kind, workspace.workspaceId, triggerPath);
72639
+ if (!remote)
72640
+ throw new Error(`${opts.kind} trigger ${triggerPath} not found`);
72641
+ await updateTrigger(opts.kind, workspace.workspaceId, triggerPath, {
72642
+ ...remote,
72643
+ permissioned_as: `u/${username}`,
72644
+ preserve_permissioned_as: true,
72645
+ path: triggerPath
72646
+ });
72647
+ info(colors.green(`Updated permissioned_as for ${opts.kind} trigger ${triggerPath} to ${email} (username: ${username})`));
72648
+ });
72120
72649
  trigger_default = command19;
72121
72650
  });
72122
72651
 
@@ -72173,14 +72702,14 @@ function showConflict(path19, local, remote) {
72173
72702
  info(`
72174
72703
  `);
72175
72704
  }
72176
- async function pushObj(workspace, p, befObj, newObj, plainSecrets, alreadySynced3, message, originalLocalPath) {
72705
+ async function pushObj(workspace, p, befObj, newObj, plainSecrets, alreadySynced3, message, originalLocalPath, permissionedAsContext) {
72177
72706
  const typeEnding = getTypeStrFromPath(p);
72178
72707
  if (typeEnding === "app") {
72179
72708
  const appName = extractResourceName(p, "app");
72180
72709
  if (!appName) {
72181
72710
  throw new Error(`Could not extract app name from path: ${p}`);
72182
72711
  }
72183
- await pushApp(workspace, appName, buildFolderPath(appName, "app"), message);
72712
+ await pushApp(workspace, appName, buildFolderPath(appName, "app"), message, permissionedAsContext);
72184
72713
  } else if (typeEnding === "raw_app") {
72185
72714
  const rawAppName = extractResourceName(p, "raw_app");
72186
72715
  if (!rawAppName) {
@@ -72196,7 +72725,7 @@ async function pushObj(workspace, p, befObj, newObj, plainSecrets, alreadySynced
72196
72725
  if (!flowName) {
72197
72726
  throw new Error(`Could not extract flow name from path: ${p}`);
72198
72727
  }
72199
- await pushFlow(workspace, flowName, buildFolderPath(flowName, "flow"), message);
72728
+ await pushFlow(workspace, flowName, buildFolderPath(flowName, "flow"), message, permissionedAsContext);
72200
72729
  } else if (typeEnding === "resource") {
72201
72730
  if (!alreadySynced3.includes(p)) {
72202
72731
  alreadySynced3.push(p);
@@ -72205,25 +72734,25 @@ async function pushObj(workspace, p, befObj, newObj, plainSecrets, alreadySynced
72205
72734
  } else if (typeEnding === "resource-type") {
72206
72735
  await pushResourceType(workspace, p, befObj, newObj);
72207
72736
  } else if (typeEnding === "schedule") {
72208
- await pushSchedule(workspace, p, befObj, newObj);
72737
+ await pushSchedule(workspace, p, befObj, newObj, permissionedAsContext);
72209
72738
  } else if (typeEnding === "http_trigger") {
72210
- await pushTrigger("http", workspace, p, befObj, newObj);
72739
+ await pushTrigger("http", workspace, p, befObj, newObj, permissionedAsContext);
72211
72740
  } else if (typeEnding === "websocket_trigger") {
72212
- await pushTrigger("websocket", workspace, p, befObj, newObj);
72741
+ await pushTrigger("websocket", workspace, p, befObj, newObj, permissionedAsContext);
72213
72742
  } else if (typeEnding === "kafka_trigger") {
72214
- await pushTrigger("kafka", workspace, p, befObj, newObj);
72743
+ await pushTrigger("kafka", workspace, p, befObj, newObj, permissionedAsContext);
72215
72744
  } else if (typeEnding === "nats_trigger") {
72216
- await pushTrigger("nats", workspace, p, befObj, newObj);
72745
+ await pushTrigger("nats", workspace, p, befObj, newObj, permissionedAsContext);
72217
72746
  } else if (typeEnding === "postgres_trigger") {
72218
- await pushTrigger("postgres", workspace, p, befObj, newObj);
72747
+ await pushTrigger("postgres", workspace, p, befObj, newObj, permissionedAsContext);
72219
72748
  } else if (typeEnding === "mqtt_trigger") {
72220
- await pushTrigger("mqtt", workspace, p, befObj, newObj);
72749
+ await pushTrigger("mqtt", workspace, p, befObj, newObj, permissionedAsContext);
72221
72750
  } else if (typeEnding === "sqs_trigger") {
72222
- await pushTrigger("sqs", workspace, p, befObj, newObj);
72751
+ await pushTrigger("sqs", workspace, p, befObj, newObj, permissionedAsContext);
72223
72752
  } else if (typeEnding === "gcp_trigger") {
72224
- await pushTrigger("gcp", workspace, p, befObj, newObj);
72753
+ await pushTrigger("gcp", workspace, p, befObj, newObj, permissionedAsContext);
72225
72754
  } else if (typeEnding === "email_trigger") {
72226
- await pushTrigger("email", workspace, p, befObj, newObj);
72755
+ await pushTrigger("email", workspace, p, befObj, newObj, permissionedAsContext);
72227
72756
  } else if (typeEnding === "native_trigger") {
72228
72757
  await pushNativeTrigger(workspace, p, befObj, newObj);
72229
72758
  } else if (typeEnding === "user") {
@@ -72547,7 +73076,7 @@ ${details.join(`
72547
73076
  `)}
72548
73077
  Use --remote to preview deployed workspace scripts instead.`);
72549
73078
  }
72550
- async function pushFlow(workspace, remotePath, localPath, message) {
73079
+ async function pushFlow(workspace, remotePath, localPath, message, permissionedAsContext) {
72551
73080
  if (alreadySynced3.includes(localPath)) {
72552
73081
  return;
72553
73082
  }
@@ -72576,6 +73105,16 @@ async function pushFlow(workspace, remotePath, localPath, message) {
72576
73105
  if (missingFiles.length > 0) {
72577
73106
  warn(colors.yellow(`Warning: missing inline script file(s): ${missingFiles.join(", ")}. ` + `The flow will be pushed with unresolved !inline references.`));
72578
73107
  }
73108
+ const hasOnBehalfOf = localFlow.has_on_behalf_of ?? !!localFlow.on_behalf_of_email;
73109
+ delete localFlow.has_on_behalf_of;
73110
+ const preserveFields = {};
73111
+ if (permissionedAsContext?.userIsAdminOrDeployer && hasOnBehalfOf) {
73112
+ if (flow && flow.on_behalf_of_email) {
73113
+ preserveFields.on_behalf_of_email = flow.on_behalf_of_email;
73114
+ preserveFields.preserve_on_behalf_of = true;
73115
+ info(`Preserving ${flow.on_behalf_of_email} as on_behalf_of for flow ${remotePath}`);
73116
+ }
73117
+ }
72579
73118
  if (flow) {
72580
73119
  if (isSuperset(localFlow, flow)) {
72581
73120
  info(colors.green(`Flow ${remotePath} is up to date`));
@@ -72588,7 +73127,8 @@ async function pushFlow(workspace, remotePath, localPath, message) {
72588
73127
  requestBody: {
72589
73128
  path: remotePath.replaceAll(SEP19, "/"),
72590
73129
  deployment_message: message,
72591
- ...localFlow
73130
+ ...localFlow,
73131
+ ...preserveFields
72592
73132
  }
72593
73133
  });
72594
73134
  } else {
@@ -72599,7 +73139,8 @@ async function pushFlow(workspace, remotePath, localPath, message) {
72599
73139
  requestBody: {
72600
73140
  path: remotePath.replaceAll(SEP19, "/"),
72601
73141
  deployment_message: message,
72602
- ...localFlow
73142
+ ...localFlow,
73143
+ ...preserveFields
72603
73144
  }
72604
73145
  });
72605
73146
  } catch (e) {
@@ -73046,7 +73587,27 @@ var init_flow = __esm(async () => {
73046
73587
  ]);
73047
73588
  import_yaml36 = __toESM(require_dist(), 1);
73048
73589
  alreadySynced3 = [];
73049
- command20 = new Command().description("flow related commands").option("--show-archived", "Enable archived flows in output").option("--json", "Output as JSON (for piping to jq)").action(list12).command("list", "list all flows").option("--show-archived", "Enable archived flows in output").option("--json", "Output as JSON (for piping to jq)").action(list12).command("get", "get a flow's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(get9).command("push", "push a local flow spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").option("--message <message:string>", "Deployment message").action(push11).command("run", "run a flow by path.").arguments("<path:string>").option("-d --data <data:string>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not ouput anything other then the final output. Useful for scripting.").action(run3).command("preview", "preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.").arguments("<flow_path:string>").option("-d --data <data:string>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other then the final output. Useful for scripting.").option("--remote", "Use deployed workspace scripts for PathScript steps instead of local files.").action(preview2).command("generate-locks", 'DEPRECATED: re-generate flow lock files. Use "wmill generate-metadata" instead.').arguments("[flow:file]").option("--yes", "Skip confirmation prompt").option("--dry-run", "Perform a dry run without making changes").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").action(generateLocks).command("new", "create a new empty flow").arguments("<flow_path:string>").option("--summary <summary:string>", "flow summary").option("--description <description:string>", "flow description").action(bootstrap2).command("bootstrap", "create a new empty flow (alias for new)").arguments("<flow_path:string>").option("--summary <summary:string>", "flow summary").option("--description <description:string>", "flow description").action(bootstrap2).command("history", "Show version history for a flow").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(history2).command("show-version", "Show a specific version of a flow").arguments("<path:string> <version:string>").option("--json", "Output as JSON (for piping to jq)").action(showVersion);
73590
+ command20 = new Command().description("flow related commands").option("--show-archived", "Enable archived flows in output").option("--json", "Output as JSON (for piping to jq)").action(list12).command("list", "list all flows").option("--show-archived", "Enable archived flows in output").option("--json", "Output as JSON (for piping to jq)").action(list12).command("get", "get a flow's details").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(get9).command("push", "push a local flow spec. This overrides any remote versions.").arguments("<file_path:string> <remote_path:string>").option("--message <message:string>", "Deployment message").action(push11).command("run", "run a flow by path.").arguments("<path:string>").option("-d --data <data:string>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not ouput anything other then the final output. Useful for scripting.").action(run3).command("preview", "preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.").arguments("<flow_path:string>").option("-d --data <data:string>", "Inputs specified as a JSON string or a file using @<filename> or stdin using @-.").option("-s --silent", "Do not output anything other then the final output. Useful for scripting.").option("--remote", "Use deployed workspace scripts for PathScript steps instead of local files.").action(preview2).command("generate-locks", 'DEPRECATED: re-generate flow lock files. Use "wmill generate-metadata" instead.').arguments("[flow:file]").option("--yes", "Skip confirmation prompt").option("--dry-run", "Perform a dry run without making changes").option("-i --includes <patterns:file[]>", "Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)").option("-e --excludes <patterns:file[]>", "Comma separated patterns to specify which file to NOT take into account.").action(generateLocks).command("new", "create a new empty flow").arguments("<flow_path:string>").option("--summary <summary:string>", "flow summary").option("--description <description:string>", "flow description").action(bootstrap2).command("bootstrap", "create a new empty flow (alias for new)").arguments("<flow_path:string>").option("--summary <summary:string>", "flow summary").option("--description <description:string>", "flow description").action(bootstrap2).command("history", "Show version history for a flow").arguments("<path:string>").option("--json", "Output as JSON (for piping to jq)").action(history2).command("show-version", "Show a specific version of a flow").arguments("<path:string> <version:string>").option("--json", "Output as JSON (for piping to jq)").action(showVersion).command("set-permissioned-as", "Set the on_behalf_of_email for a flow (requires admin or wm_deployers group)").arguments("<path:string> <email:string>").action(async (opts, flowPath, email) => {
73591
+ const workspace = await resolveWorkspace(opts);
73592
+ await requireLogin(opts);
73593
+ const remote = await getFlowByPath({
73594
+ workspace: workspace.workspaceId,
73595
+ path: flowPath
73596
+ });
73597
+ if (!remote)
73598
+ throw new Error(`Flow ${flowPath} not found`);
73599
+ await updateFlow({
73600
+ workspace: workspace.workspaceId,
73601
+ path: flowPath,
73602
+ requestBody: {
73603
+ ...remote,
73604
+ path: flowPath,
73605
+ on_behalf_of_email: email,
73606
+ preserve_on_behalf_of: true
73607
+ }
73608
+ });
73609
+ info(colors.green(`Updated permissioned_as for flow ${flowPath} to ${email}`));
73610
+ });
73050
73611
  flow_default = command20;
73051
73612
  });
73052
73613
 
@@ -76044,7 +76605,59 @@ await __promiseAll([
76044
76605
  init_workspace(),
76045
76606
  init_resource_type()
76046
76607
  ]);
76047
- import { stat as stat17, writeFile as writeFile19, rm as rm4, mkdir as mkdir13 } from "node:fs/promises";
76608
+ import { stat as stat18, writeFile as writeFile20, rm as rm4 } from "node:fs/promises";
76609
+
76610
+ // src/guidance/writer.ts
76611
+ import { cp, mkdir as mkdir13, readdir as readdir9, readFile as readFile17, stat as stat17, writeFile as writeFile19 } from "node:fs/promises";
76612
+ import { join as join17 } from "node:path";
76613
+
76614
+ // src/guidance/core.ts
76615
+ function generateAgentsMdContent(skillsReference) {
76616
+ return `# Windmill AI Agent Instructions
76617
+
76618
+ You are a helpful assistant that can help with Windmill scripts, flows, apps, and resources management.
76619
+
76620
+ ## Important Notes
76621
+ - Every new entity MUST be created using the skills listed below.
76622
+ - Every modification of an entity MUST be done using the skills listed below.
76623
+ - User MUST be asked where to create the entity. It can be in its user folder, under u/{user_name} folder, or in a new folder, /f/{folder_name}/. folder_name and user_name must be provided by the user.
76624
+
76625
+ ## Script Writing Guide
76626
+
76627
+ You MUST use the \`write-script-<language>\` skill to write or modify scripts in the language specified by the user. Use bun by default.
76628
+
76629
+ ## Flow Writing Guide
76630
+
76631
+ You MUST use the \`write-flow\` skill to create or modify flows.
76632
+
76633
+ ## Raw App Development
76634
+
76635
+ You MUST use the \`raw-app\` skill to create or modify raw apps.
76636
+ Whenever a new app needs to be created you MUST ask the user to run \`wmill app new\` in its terminal first.
76637
+
76638
+ ## Triggers
76639
+
76640
+ You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, GCP, or Postgres CDC triggers.
76641
+
76642
+ ## Schedules
76643
+
76644
+ You MUST use the \`schedules\` skill to configure cron schedules.
76645
+
76646
+ ## Resources
76647
+
76648
+ You MUST use the \`resources\` skill to manage resource types and credentials.
76649
+
76650
+ ## CLI Reference
76651
+
76652
+ You MUST use the \`cli-commands\` skill to use the CLI.
76653
+
76654
+ ## Skills
76655
+
76656
+ For specific guidance, ALWAYS use the skills listed below.
76657
+
76658
+ ${skillsReference}
76659
+ `;
76660
+ }
76048
76661
 
76049
76662
  // src/guidance/skills.ts
76050
76663
  var SKILLS = [
@@ -81148,6 +81761,7 @@ app related commands
81148
81761
  - \`--fix\` - Attempt to fix common issues (not implemented yet)
81149
81762
  - \`app new\` - create a new raw app from a template
81150
81763
  - \`app generate-agents [app_folder:string]\` - regenerate AGENTS.md and DATATABLES.md from remote workspace
81764
+ - \`app set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for an app (requires admin or wm_deployers group)
81151
81765
 
81152
81766
  ### audit
81153
81767
 
@@ -81230,6 +81844,7 @@ flow related commands
81230
81844
  - \`--json\` - Output as JSON (for piping to jq)
81231
81845
  - \`flow show-version <path:string> <version:string>\` - Show a specific version of a flow
81232
81846
  - \`--json\` - Output as JSON (for piping to jq)
81847
+ - \`flow set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for a flow (requires admin or wm_deployers group)
81233
81848
 
81234
81849
  ### folder
81235
81850
 
@@ -81249,6 +81864,9 @@ folder related commands
81249
81864
  - \`folder push <name:string>\` - push a local folder to the remote by name. This overrides any remote versions.
81250
81865
  - \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
81251
81866
  - \`-y, --yes\` - skip confirmation prompt
81867
+ - \`folder show-rules <name:string>\` - Show default_permissioned_as rules for a folder. Use --test-path to see which rule matches a given item path.
81868
+ - \`--test-path <path:string>\` - Test which rule matches this item path (e.g. f/prod/jobs/my_script)
81869
+ - \`--json\` - Output as JSON
81252
81870
 
81253
81871
  ### generate-metadata
81254
81872
 
@@ -81472,6 +82090,7 @@ schedule related commands
81472
82090
  - \`schedule push <file_path:string> <remote_path:string>\` - push a local schedule spec. This overrides any remote versions.
81473
82091
  - \`schedule enable <path:string>\` - Enable a schedule
81474
82092
  - \`schedule disable <path:string>\` - Disable a schedule
82093
+ - \`schedule set-permissioned-as <path:string> <email:string>\` - Set the email (run-as user) for a schedule (requires admin or wm_deployers group)
81475
82094
 
81476
82095
  ### script
81477
82096
 
@@ -81503,6 +82122,7 @@ script related commands
81503
82122
  - \`script bootstrap <path:file> <language:string>\` - create a new script (alias for new)
81504
82123
  - \`--summary <summary:string>\` - script summary
81505
82124
  - \`--description <description:string>\` - script description
82125
+ - \`script set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for a script (requires admin or wm_deployers group)
81506
82126
  - \`script history <path:string>\` - show version history for a script
81507
82127
  - \`--json\` - Output as JSON (for piping to jq)
81508
82128
 
@@ -81576,6 +82196,7 @@ sync local with a remote workspaces or the opposite (push or pull)
81576
82196
  - \`--lint\` - Run lint validation before pushing
81577
82197
  - \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
81578
82198
  - \`--auto-metadata\` - Automatically regenerate stale metadata (locks and schemas) before pushing
82199
+ - \`--accept-overriding-permissioned-as-with-self\` - Accept that items with a different permissioned_as will be updated with your own user
81579
82200
 
81580
82201
  ### token
81581
82202
 
@@ -81610,6 +82231,8 @@ trigger related commands
81610
82231
  - \`trigger new <path:string>\` - create a new trigger locally
81611
82232
  - \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)
81612
82233
  - \`trigger push <file_path:string> <remote_path:string>\` - push a local trigger spec. This overrides any remote versions.
82234
+ - \`trigger set-permissioned-as <path:string> <email:string>\` - Set the email (run-as user) for a trigger (requires admin or wm_deployers group)
82235
+ - \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email)
81613
82236
 
81614
82237
  ### user
81615
82238
 
@@ -82647,52 +83270,165 @@ var SCHEMA_MAPPINGS = {
82647
83270
  ]
82648
83271
  };
82649
83272
 
82650
- // src/guidance/core.ts
82651
- function generateAgentsMdContent(skillsReference) {
82652
- return `# Windmill AI Agent Instructions
82653
-
82654
- You are a helpful assistant that can help with Windmill scripts, flows, apps, and resources management.
82655
-
82656
- ## Important Notes
82657
- - Every new entity MUST be created using the skills listed below.
82658
- - Every modification of an entity MUST be done using the skills listed below.
82659
- - User MUST be asked where to create the entity. It can be in its user folder, under u/{user_name} folder, or in a new folder, /f/{folder_name}/. folder_name and user_name must be provided by the user.
82660
-
82661
- ## Script Writing Guide
82662
-
82663
- You MUST use the \`write-script-<language>\` skill to write or modify scripts in the language specified by the user. Use bun by default.
82664
-
82665
- ## Flow Writing Guide
82666
-
82667
- You MUST use the \`write-flow\` skill to create or modify flows.
82668
-
82669
- ## Raw App Development
82670
-
82671
- You MUST use the \`raw-app\` skill to create or modify raw apps.
82672
- Whenever a new app needs to be created you MUST ask the user to run \`wmill app new\` in its terminal first.
82673
-
82674
- ## Triggers
82675
-
82676
- You MUST use the \`triggers\` skill to configure HTTP routes, WebSocket, Kafka, NATS, SQS, MQTT, GCP, or Postgres CDC triggers.
82677
-
82678
- ## Schedules
82679
-
82680
- You MUST use the \`schedules\` skill to configure cron schedules.
82681
-
82682
- ## Resources
82683
-
82684
- You MUST use the \`resources\` skill to manage resource types and credentials.
82685
-
82686
- ## CLI Reference
83273
+ // src/guidance/writer.ts
83274
+ var WMILL_INIT_AI_SKILLS_SOURCE_ENV = "WMILL_INIT_AI_SKILLS_SOURCE";
83275
+ var WMILL_INIT_AI_AGENTS_SOURCE_ENV = "WMILL_INIT_AI_AGENTS_SOURCE";
83276
+ var WMILL_INIT_AI_CLAUDE_SOURCE_ENV = "WMILL_INIT_AI_CLAUDE_SOURCE";
83277
+ var CLAUDE_MD_DEFAULT = `Instructions are in @AGENTS.md
83278
+ `;
83279
+ async function writeAiGuidanceFiles(options) {
83280
+ const nonDottedPaths = options.nonDottedPaths ?? true;
83281
+ const skillMetadata = options.skillsSourcePath ? await readSkillMetadataFromDirectory(options.skillsSourcePath) : getGeneratedSkillMetadata();
83282
+ const agentsWritten = await writeProjectGuidanceFile({
83283
+ targetPath: join17(options.targetDir, "AGENTS.md"),
83284
+ overwrite: options.overwriteProjectGuidance ?? false,
83285
+ content: options.agentsSourcePath != null ? await readFile17(options.agentsSourcePath, "utf8") : generateAgentsMdContent(buildSkillsReference(skillMetadata))
83286
+ });
83287
+ const claudeWritten = await writeProjectGuidanceFile({
83288
+ targetPath: join17(options.targetDir, "CLAUDE.md"),
83289
+ overwrite: options.overwriteProjectGuidance ?? false,
83290
+ content: options.claudeSourcePath != null ? await readFile17(options.claudeSourcePath, "utf8") : CLAUDE_MD_DEFAULT
83291
+ });
83292
+ if (options.skillsSourcePath) {
83293
+ await copySkillsFromSource(options.targetDir, options.skillsSourcePath);
83294
+ } else {
83295
+ await writeGeneratedSkills(options.targetDir, nonDottedPaths);
83296
+ }
83297
+ return {
83298
+ agentsWritten,
83299
+ claudeWritten,
83300
+ skillCount: skillMetadata.length
83301
+ };
83302
+ }
83303
+ function buildSkillsReference(skills) {
83304
+ return skills.map((skill) => `- \`.claude/skills/${skill.directoryName}/SKILL.md\` - ${skill.description}`).join(`
83305
+ `);
83306
+ }
83307
+ async function copySkillsFromSource(targetDir, skillsSourcePath) {
83308
+ const skillsDir = await ensureSkillsDirectory(targetDir);
83309
+ await copyDirectoryContents(skillsSourcePath, skillsDir);
83310
+ return await readSkillMetadataFromDirectory(skillsDir);
83311
+ }
83312
+ async function writeGeneratedSkills(targetDir, nonDottedPaths) {
83313
+ const skillsDir = await ensureSkillsDirectory(targetDir);
83314
+ await Promise.all(SKILLS.map(async (skill) => {
83315
+ const skillDir = join17(skillsDir, skill.name);
83316
+ await mkdir13(skillDir, { recursive: true });
83317
+ await writeFile19(join17(skillDir, "SKILL.md"), renderGeneratedSkillContent(skill.name, nonDottedPaths), "utf8");
83318
+ }));
83319
+ return SKILLS.map((skill) => ({
83320
+ ...skill,
83321
+ directoryName: skill.name
83322
+ }));
83323
+ }
83324
+ function getGeneratedSkillMetadata() {
83325
+ return SKILLS.map((skill) => ({
83326
+ ...skill,
83327
+ directoryName: skill.name
83328
+ }));
83329
+ }
83330
+ async function ensureSkillsDirectory(targetDir) {
83331
+ const skillsDir = join17(targetDir, ".claude", "skills");
83332
+ await mkdir13(skillsDir, { recursive: true });
83333
+ return skillsDir;
83334
+ }
83335
+ async function copyDirectoryContents(sourceDir, targetDir) {
83336
+ const entries = await readdir9(sourceDir, { withFileTypes: true });
83337
+ await Promise.all(entries.map(async (entry) => {
83338
+ await cp(join17(sourceDir, entry.name), join17(targetDir, entry.name), {
83339
+ recursive: true,
83340
+ force: true
83341
+ });
83342
+ }));
83343
+ }
83344
+ function renderGeneratedSkillContent(skillName, nonDottedPaths) {
83345
+ let skillContent = SKILL_CONTENT[skillName];
83346
+ if (!skillContent) {
83347
+ throw new Error(`Missing generated skill content for ${skillName}`);
83348
+ }
83349
+ if (nonDottedPaths) {
83350
+ skillContent = skillContent.replaceAll("{{FLOW_SUFFIX}}", "__flow").replaceAll("{{APP_SUFFIX}}", "__app").replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app").replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).");
83351
+ } else {
83352
+ skillContent = skillContent.replaceAll("{{FLOW_SUFFIX}}", ".flow").replaceAll("{{APP_SUFFIX}}", ".app").replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app").replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`).");
83353
+ }
83354
+ const schemaMappings = SCHEMA_MAPPINGS[skillName];
83355
+ if (!schemaMappings || schemaMappings.length === 0) {
83356
+ return skillContent;
83357
+ }
83358
+ const schemaDocs = schemaMappings.map((mapping) => {
83359
+ const schemaYaml = SCHEMAS[mapping.schemaKey];
83360
+ if (!schemaYaml) {
83361
+ return null;
83362
+ }
83363
+ return formatSchemaForMarkdown(schemaYaml, mapping.name, mapping.filePattern);
83364
+ }).filter((entry) => entry !== null);
83365
+ if (schemaDocs.length === 0) {
83366
+ return skillContent;
83367
+ }
83368
+ return `${skillContent}
82687
83369
 
82688
- You MUST use the \`cli-commands\` skill to use the CLI.
83370
+ ${schemaDocs.join(`
82689
83371
 
82690
- ## Skills
83372
+ `)}`;
83373
+ }
83374
+ async function readSkillMetadataFromDirectory(skillsDir) {
83375
+ const entries = await readdir9(skillsDir, { withFileTypes: true });
83376
+ const skills = [];
83377
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
83378
+ if (!entry.isDirectory()) {
83379
+ continue;
83380
+ }
83381
+ const skillPath = join17(skillsDir, entry.name, "SKILL.md");
83382
+ if (!await stat17(skillPath).catch(() => null)) {
83383
+ continue;
83384
+ }
83385
+ const content = await readFile17(skillPath, "utf8");
83386
+ skills.push(parseSkillMetadata(content, entry.name));
83387
+ }
83388
+ return skills;
83389
+ }
83390
+ function parseSkillMetadata(content, fallbackName) {
83391
+ const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
83392
+ if (!frontMatterMatch) {
83393
+ return {
83394
+ name: fallbackName,
83395
+ description: `Skill loaded from ${fallbackName}`,
83396
+ directoryName: fallbackName
83397
+ };
83398
+ }
83399
+ let name = fallbackName;
83400
+ let description = `Skill loaded from ${fallbackName}`;
83401
+ for (const line of frontMatterMatch[1].split(`
83402
+ `)) {
83403
+ const separatorIndex = line.indexOf(":");
83404
+ if (separatorIndex === -1) {
83405
+ continue;
83406
+ }
83407
+ const key = line.slice(0, separatorIndex).trim();
83408
+ const value = line.slice(separatorIndex + 1).trim();
83409
+ if (key === "name" && value) {
83410
+ name = value;
83411
+ } else if (key === "description" && value) {
83412
+ description = value;
83413
+ }
83414
+ }
83415
+ return { name, description, directoryName: fallbackName };
83416
+ }
83417
+ async function writeProjectGuidanceFile(options) {
83418
+ if (!options.overwrite && await stat17(options.targetPath).catch(() => null)) {
83419
+ return false;
83420
+ }
83421
+ await writeFile19(options.targetPath, options.content, "utf8");
83422
+ return true;
83423
+ }
83424
+ function formatSchemaForMarkdown(schemaYaml, schemaName, filePattern) {
83425
+ return `## ${schemaName} (\`${filePattern}\`)
82691
83426
 
82692
- For specific guidance, ALWAYS use the skills listed below.
83427
+ Must be a YAML file that adheres to the following schema:
82693
83428
 
82694
- ${skillsReference}
82695
- `;
83429
+ \`\`\`yaml
83430
+ ${schemaYaml.trim()}
83431
+ \`\`\``;
82696
83432
  }
82697
83433
 
82698
83434
  // src/commands/init/template.ts
@@ -82893,6 +83629,13 @@ var CONFIG_REFERENCE = [
82893
83629
  description: "Use __flow/__app/__raw_app suffixes instead of .flow/.app/.raw_app",
82894
83630
  inlineComment: "recommended for new projects"
82895
83631
  },
83632
+ {
83633
+ name: "syncBehavior",
83634
+ type: "string",
83635
+ default: "v1",
83636
+ description: "Sync behavior version — controls ownership handling during push/pull (v1: preserve permissioned_as on update, strip on_behalf_of_email on pull)",
83637
+ inlineComment: "v1 enables ownership preservation"
83638
+ },
82896
83639
  {
82897
83640
  name: "codebases",
82898
83641
  type: "array",
@@ -83156,19 +83899,10 @@ function formatConfigReferenceJson() {
83156
83899
  }
83157
83900
 
83158
83901
  // src/commands/init/init.ts
83159
- function formatSchemaForMarkdown(schemaYaml, schemaName, filePattern) {
83160
- return `## ${schemaName} (\`${filePattern}\`)
83161
-
83162
- Must be a YAML file that adheres to the following schema:
83163
-
83164
- \`\`\`yaml
83165
- ${schemaYaml.trim()}
83166
- \`\`\``;
83167
- }
83168
83902
  async function initAction(opts) {
83169
83903
  let didBindWorkspace = false;
83170
83904
  let boundProfile;
83171
- if (await stat17("wmill.yaml").catch(() => null)) {
83905
+ if (await stat18("wmill.yaml").catch(() => null)) {
83172
83906
  info("wmill.yaml already exists, skipping config generation");
83173
83907
  } else {
83174
83908
  const { isGitRepository: isGitRepository2, getCurrentGitBranch: getCurrentGitBranch2 } = await Promise.resolve().then(() => (init_git(), exports_git));
@@ -83240,7 +83974,7 @@ async function initAction(opts) {
83240
83974
  boundProfile = activeWorkspace;
83241
83975
  }
83242
83976
  }
83243
- await writeFile19("wmill.yaml", generateCommentedTemplate(branchName, undefined, wsBindings), "utf-8");
83977
+ await writeFile20("wmill.yaml", generateCommentedTemplate(branchName, undefined, wsBindings), "utf-8");
83244
83978
  info(colors.green("wmill.yaml created with default settings"));
83245
83979
  if (wsBindings && wsBindings.length > 0) {
83246
83980
  didBindWorkspace = true;
@@ -83312,58 +84046,21 @@ async function initAction(opts) {
83312
84046
  nonDottedPaths = config.nonDottedPaths ?? true;
83313
84047
  } catch {}
83314
84048
  try {
83315
- const skills_base_dir = ".claude/skills";
83316
- const skillsReference = SKILLS.map((s) => `- \`${skills_base_dir}/${s.name}/SKILL.md\` - ${s.description}`).join(`
83317
- `);
83318
- if (!await stat17("AGENTS.md").catch(() => null)) {
83319
- await writeFile19("AGENTS.md", generateAgentsMdContent(skillsReference), "utf-8");
84049
+ const guidanceResult = await writeAiGuidanceFiles({
84050
+ targetDir: ".",
84051
+ nonDottedPaths,
84052
+ overwriteProjectGuidance: false,
84053
+ skillsSourcePath: process.env[WMILL_INIT_AI_SKILLS_SOURCE_ENV],
84054
+ agentsSourcePath: process.env[WMILL_INIT_AI_AGENTS_SOURCE_ENV],
84055
+ claudeSourcePath: process.env[WMILL_INIT_AI_CLAUDE_SOURCE_ENV]
84056
+ });
84057
+ if (guidanceResult.agentsWritten) {
83320
84058
  info(colors.green("Created AGENTS.md"));
83321
84059
  }
83322
- if (!await stat17("CLAUDE.md").catch(() => null)) {
83323
- await writeFile19("CLAUDE.md", `Instructions are in @AGENTS.md
83324
- `, "utf-8");
84060
+ if (guidanceResult.claudeWritten) {
83325
84061
  info(colors.green("Created CLAUDE.md"));
83326
84062
  }
83327
- try {
83328
- await mkdir13(".claude/skills", { recursive: true });
83329
- await Promise.all(SKILLS.map(async (skill) => {
83330
- const skillDir = `.claude/skills/${skill.name}`;
83331
- await mkdir13(skillDir, { recursive: true });
83332
- let skillContent = SKILL_CONTENT[skill.name];
83333
- if (skillContent) {
83334
- if (nonDottedPaths) {
83335
- skillContent = skillContent.replaceAll("{{FLOW_SUFFIX}}", "__flow").replaceAll("{{APP_SUFFIX}}", "__app").replaceAll("{{RAW_APP_SUFFIX}}", "__raw_app").replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).");
83336
- } else {
83337
- skillContent = skillContent.replaceAll("{{FLOW_SUFFIX}}", ".flow").replaceAll("{{APP_SUFFIX}}", ".app").replaceAll("{{RAW_APP_SUFFIX}}", ".raw_app").replaceAll("{{INLINE_SCRIPT_NAMING}}", "Inline script files use the `.inline_script.` naming convention (e.g. `a.inline_script.ts`).");
83338
- }
83339
- const schemaMappings = SCHEMA_MAPPINGS[skill.name];
83340
- if (schemaMappings && schemaMappings.length > 0) {
83341
- const schemaDocs = schemaMappings.map((mapping) => {
83342
- const schemaYaml = SCHEMAS[mapping.schemaKey];
83343
- if (schemaYaml) {
83344
- return formatSchemaForMarkdown(schemaYaml, mapping.name, mapping.filePattern);
83345
- }
83346
- return null;
83347
- }).filter((doc) => doc !== null);
83348
- if (schemaDocs.length > 0) {
83349
- skillContent = skillContent + `
83350
-
83351
- ` + schemaDocs.join(`
83352
-
83353
- `);
83354
- }
83355
- }
83356
- await writeFile19(`${skillDir}/SKILL.md`, skillContent, "utf-8");
83357
- }
83358
- }));
83359
- info(colors.green(`Created .claude/skills/ with ${SKILLS.length} skills`));
83360
- } catch (skillError) {
83361
- if (skillError instanceof Error) {
83362
- warn(`Could not create skills: ${skillError.message}`);
83363
- } else {
83364
- warn(`Could not create skills: ${skillError}`);
83365
- }
83366
- }
84063
+ info(colors.green(`Created .claude/skills/ with ${guidanceResult.skillCount} skills`));
83367
84064
  } catch (error2) {
83368
84065
  if (error2 instanceof Error) {
83369
84066
  warn(`Could not create guidance files: ${error2.message}`);
@@ -84406,7 +85103,7 @@ init_mod3();
84406
85103
  init_log();
84407
85104
  init_colors2();
84408
85105
  var import_yaml40 = __toESM(require_dist(), 1);
84409
- import { writeFile as writeFile21 } from "node:fs/promises";
85106
+ import { writeFile as writeFile22 } from "node:fs/promises";
84410
85107
  init_yaml();
84411
85108
  await init_conf();
84412
85109
  async function configAction(opts) {
@@ -84458,7 +85155,7 @@ async function migrateAction() {
84458
85155
  } else {
84459
85156
  newConf.workspaces = workspaces;
84460
85157
  }
84461
- await writeFile21(wmillYamlPath, import_yaml40.stringify(newConf), "utf-8");
85158
+ await writeFile22(wmillYamlPath, import_yaml40.stringify(newConf), "utf-8");
84462
85159
  info(colors.green(`✅ Migrated '${legacyKey}' to 'workspaces' in ${wmillYamlPath}`));
84463
85160
  if (wsNames.length > 0) {
84464
85161
  info(` Workspace entries: ${wsNames.join(", ")}`);
@@ -84472,8 +85169,36 @@ var config_default = command35;
84472
85169
 
84473
85170
  // src/main.ts
84474
85171
  await init_context();
84475
- var VERSION = "1.681.0";
84476
- var command36 = new Command().name("wmill").action(() => info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)).description("Windmill CLI").globalOption("--workspace <workspace:string>", "Specify the target workspace. This overrides the default workspace.").globalOption("--debug --verbose", "Show debug/verbose logs").globalOption("--show-diffs", "Show diff informations when syncing (may show sensitive informations)").globalOption("--token <token:string>", "Specify an API token. This will override any stored token.").globalOption("--base-url <baseUrl:string>", "Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.").globalOption("--config-dir <configDir:string>", "Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.").env("HEADERS <headers:string>", `Specify headers to use for all requests. e.g: "HEADERS='h1: v1, h2: v2'"`).version(VERSION).versionOption(false).command("init", init_default).command("app", app_default).command("flow", flow_default).command("script", script_default).command("workspace", workspace_default).command("resource", resource_default).command("resource-type", resource_type_default).command("user", user_default).command("variable", variable_default).command("hub", hub_default).command("folder", folder_default).command("schedule", schedule_default).command("trigger", trigger_default).command("dev", dev_default2).command("sync", sync_default).command("lint", lint_default).command("gitsync-settings", gitsync_settings_default).command("instance", instance_default).command("worker-groups", worker_groups_default).command("workers", workers_default).command("queues", queues_default).command("dependencies", dependencies_default).command("jobs", jobs_default).command("job", job_default).command("group", group_default).command("audit", audit_default).command("token", token_default).command("generate-metadata", generate_metadata_default).command("docs", docs_default).command("config", config_default).command("version --version", "Show version information").action(async (opts) => {
85172
+ var VERSION = "1.683.0";
85173
+ async function checkVersionSafe(cmd) {
85174
+ const mainCommand = cmd.getMainCommand();
85175
+ const upgradeCommand = mainCommand.getCommand("upgrade");
85176
+ if (!upgradeCommand || typeof upgradeCommand.getLatestVersion !== "function" || typeof upgradeCommand.hasRequiredPermissions !== "function") {
85177
+ return;
85178
+ }
85179
+ if (!await upgradeCommand.hasRequiredPermissions()) {
85180
+ return;
85181
+ }
85182
+ const latestVersion = await upgradeCommand.getLatestVersion();
85183
+ const currentVersion = mainCommand.getVersion();
85184
+ if (!currentVersion || currentVersion === latestVersion) {
85185
+ return;
85186
+ }
85187
+ mainCommand.version(`${currentVersion} (New version available: ${latestVersion}. Run '${mainCommand.getName()} upgrade' to upgrade to the latest version!)`);
85188
+ }
85189
+ var command36 = new Command().name("wmill").action(() => info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)).description("Windmill CLI").globalOption("--workspace <workspace:string>", "Specify the target workspace. This overrides the default workspace.").globalOption("--debug --verbose", "Show debug/verbose logs").globalOption("--show-diffs", "Show diff informations when syncing (may show sensitive informations)").globalOption("--token <token:string>", "Specify an API token. This will override any stored token.").globalOption("--base-url <baseUrl:string>", "Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.").globalOption("--config-dir <configDir:string>", "Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.").env("HEADERS <headers:string>", `Specify headers to use for all requests. e.g: "HEADERS='h1: v1, h2: v2'"`).version(VERSION).versionOption(false).helpOption("-h, --help", "Show this help.", {
85190
+ action: async function() {
85191
+ const self2 = this;
85192
+ const long = self2.getRawArgs().includes(`--${self2.getHelpOption()?.name}`);
85193
+ try {
85194
+ await checkVersionSafe(self2);
85195
+ } catch (e) {
85196
+ warn(`Skipping latest-version check: ${e instanceof Error ? e.message : String(e)}`);
85197
+ }
85198
+ self2.showHelp({ long });
85199
+ self2.exit();
85200
+ }
85201
+ }).command("init", init_default).command("app", app_default).command("flow", flow_default).command("script", script_default).command("workspace", workspace_default).command("resource", resource_default).command("resource-type", resource_type_default).command("user", user_default).command("variable", variable_default).command("hub", hub_default).command("folder", folder_default).command("schedule", schedule_default).command("trigger", trigger_default).command("dev", dev_default2).command("sync", sync_default).command("lint", lint_default).command("gitsync-settings", gitsync_settings_default).command("instance", instance_default).command("worker-groups", worker_groups_default).command("workers", workers_default).command("queues", queues_default).command("dependencies", dependencies_default).command("jobs", jobs_default).command("job", job_default).command("group", group_default).command("audit", audit_default).command("token", token_default).command("generate-metadata", generate_metadata_default).command("docs", docs_default).command("config", config_default).command("version --version", "Show version information").action(async (opts) => {
84477
85202
  console.log("CLI version: " + VERSION);
84478
85203
  try {
84479
85204
  const provider2 = new NpmProvider({ package: "windmill-cli" });