windmill-cli 1.589.3 → 1.590.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 (54) hide show
  1. package/esm/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.js +4 -4
  2. package/esm/deps.js +1 -1
  3. package/esm/gen/core/OpenAPI.js +1 -1
  4. package/esm/gen/services.gen.js +149 -27
  5. package/esm/src/commands/app/app_metadata.js +81 -47
  6. package/esm/src/commands/app/apps.js +2 -0
  7. package/esm/src/commands/app/bundle.js +6 -2
  8. package/esm/src/commands/app/dev.js +40 -14
  9. package/esm/src/commands/app/lint.js +159 -0
  10. package/esm/src/commands/app/raw_apps.js +169 -24
  11. package/esm/src/commands/init/init.js +8 -0
  12. package/esm/src/commands/resource-type/resource-type.js +29 -2
  13. package/esm/src/commands/script/script.js +11 -0
  14. package/esm/src/commands/sync/sync.js +49 -17
  15. package/esm/src/main.js +1 -1
  16. package/esm/src/utils/resource_types.js +32 -0
  17. package/esm/src/utils/utils.js +8 -0
  18. package/esm/windmill-utils-internal/src/path-utils/path-assigner.js +78 -0
  19. package/package.json +1 -1
  20. package/types/bootstrap/common.d.ts +12 -0
  21. package/types/bootstrap/common.d.ts.map +1 -1
  22. package/types/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.d.ts.map +1 -1
  23. package/types/deps.d.ts +1 -1
  24. package/types/gen/services.gen.d.ts +74 -16
  25. package/types/gen/services.gen.d.ts.map +1 -1
  26. package/types/gen/types.gen.d.ts +568 -62
  27. package/types/gen/types.gen.d.ts.map +1 -1
  28. package/types/src/commands/app/app_metadata.d.ts +1 -1
  29. package/types/src/commands/app/app_metadata.d.ts.map +1 -1
  30. package/types/src/commands/app/apps.d.ts.map +1 -1
  31. package/types/src/commands/app/bundle.d.ts.map +1 -1
  32. package/types/src/commands/app/dev.d.ts.map +1 -1
  33. package/types/src/commands/app/lint.d.ts +12 -0
  34. package/types/src/commands/app/lint.d.ts.map +1 -0
  35. package/types/src/commands/app/metadata.d.ts +2 -3
  36. package/types/src/commands/app/metadata.d.ts.map +1 -1
  37. package/types/src/commands/app/raw_apps.d.ts +26 -1
  38. package/types/src/commands/app/raw_apps.d.ts.map +1 -1
  39. package/types/src/commands/init/init.d.ts.map +1 -1
  40. package/types/src/commands/resource-type/resource-type.d.ts +3 -1
  41. package/types/src/commands/resource-type/resource-type.d.ts.map +1 -1
  42. package/types/src/commands/script/script.d.ts +5 -0
  43. package/types/src/commands/script/script.d.ts.map +1 -1
  44. package/types/src/commands/sync/sync.d.ts.map +1 -1
  45. package/types/src/main.d.ts +1 -1
  46. package/types/src/utils/resource_types.d.ts +3 -0
  47. package/types/src/utils/resource_types.d.ts.map +1 -0
  48. package/types/src/utils/utils.d.ts +2 -0
  49. package/types/src/utils/utils.d.ts.map +1 -1
  50. package/types/windmill-utils-internal/src/gen/types.gen.d.ts +568 -62
  51. package/types/windmill-utils-internal/src/gen/types.gen.d.ts.map +1 -1
  52. package/types/windmill-utils-internal/src/path-utils/path-assigner.d.ts +22 -0
  53. package/types/windmill-utils-internal/src/path-utils/path-assigner.d.ts.map +1 -1
  54. /package/types/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.d.ts +0 -0
@@ -15,6 +15,7 @@ import { requireLogin } from "../../core/auth.js";
15
15
  import { GLOBAL_CONFIG_OPT } from "../../core/conf.js";
16
16
  import { replaceInlineScripts } from "./apps.js";
17
17
  import { APP_BACKEND_FOLDER, inferRunnableSchemaFromFile, } from "./app_metadata.js";
18
+ import { loadRunnablesFromBackend } from "./raw_apps.js";
18
19
  const DEFAULT_PORT = 4000;
19
20
  const DEFAULT_HOST = "localhost";
20
21
  // HTML template with live reload
@@ -128,8 +129,12 @@ async function dev(opts) {
128
129
  const wmillPlugin = {
129
130
  name: "wmill-virtual",
130
131
  setup(build) {
131
- // Intercept imports of /wmill.ts, /wmill, ./wmill.ts, or ./wmill
132
- build.onResolve({ filter: /^(\.\/|\/)?wmill(\.ts)?$/ }, (args) => {
132
+ // Intercept imports of wmill with various path formats:
133
+ // - wmill, wmill.ts (bare import)
134
+ // - /wmill, /wmill.ts (absolute)
135
+ // - ./wmill, ./wmill.ts (same directory)
136
+ // - ../wmill, ../../wmill, etc. (parent directories)
137
+ build.onResolve({ filter: /^(\.\.\/)+wmill(\.ts)?$|^(\.\/|\/)?wmill(\.ts)?$/ }, (args) => {
133
138
  log.info(colors.yellow(`[wmill-virtual] Intercepted: ${args.path}`));
134
139
  return {
135
140
  path: args.path,
@@ -469,16 +474,30 @@ const command = new Command()
469
474
  export default command;
470
475
  /**
471
476
  * Generates wmill.d.ts with type definitions for runnables.
472
- * Merges in-memory inferred schemas with runnables from raw_app.yaml.
477
+ * Loads runnables from separate YAML files in the backend folder (new format)
478
+ * or falls back to raw_app.yaml (old format).
479
+ * Merges in-memory inferred schemas with runnables.
473
480
  *
474
481
  * @param schemaOverrides - In-memory schema overrides (runnableId -> schema)
475
482
  */
476
483
  async function genRunnablesTs(schemaOverrides = {}) {
477
484
  log.info(colors.blue("🔄 Generating wmill.d.ts..."));
478
- const rawApp = (await yamlParseFile(path.join(process.cwd(), "raw_app.yaml")));
479
- const runnables = rawApp?.["runnables"];
485
+ const localPath = process.cwd();
486
+ const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
487
+ // Load runnables from separate files (new format) or fall back to raw_app.yaml (old format)
488
+ let runnables = await loadRunnablesFromBackend(backendPath);
489
+ if (Object.keys(runnables).length === 0) {
490
+ // Fall back to old format
491
+ try {
492
+ const rawApp = (await yamlParseFile(path.join(localPath, "raw_app.yaml")));
493
+ runnables = rawApp?.["runnables"] ?? {};
494
+ }
495
+ catch {
496
+ runnables = {};
497
+ }
498
+ }
480
499
  // Apply schema overrides from in-memory cache
481
- if (runnables && Object.keys(schemaOverrides).length > 0) {
500
+ if (Object.keys(schemaOverrides).length > 0) {
482
501
  for (const [runnableId, schema] of Object.entries(schemaOverrides)) {
483
502
  if (runnables[runnableId]?.inlineScript) {
484
503
  runnables[runnableId].inlineScript.schema = schema;
@@ -497,9 +516,16 @@ async function genRunnablesTs(schemaOverrides = {}) {
497
516
  async function loadRunnables() {
498
517
  try {
499
518
  const localPath = process.cwd();
500
- const rawApp = (await yamlParseFile(path.join(localPath, "raw_app.yaml")));
501
- replaceInlineScripts(rawApp.runnables, path.join(localPath, APP_BACKEND_FOLDER) + SEP, true);
502
- return rawApp?.runnables ?? {};
519
+ const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
520
+ // Load runnables from separate files (new format) or fall back to raw_app.yaml (old format)
521
+ let runnables = await loadRunnablesFromBackend(backendPath);
522
+ if (Object.keys(runnables).length === 0) {
523
+ // Fall back to old format
524
+ const rawApp = (await yamlParseFile(path.join(localPath, "raw_app.yaml")));
525
+ runnables = rawApp?.runnables ?? {};
526
+ }
527
+ replaceInlineScripts(runnables, backendPath + SEP, true);
528
+ return runnables;
503
529
  }
504
530
  catch (error) {
505
531
  log.error(colors.red(`Failed to load runnables: ${error.message}`));
@@ -539,12 +565,12 @@ async function executeRunnable(runnable, workspace, appPath, runnableId, args) {
539
565
  cache_ttl: inlineScript.cache_ttl,
540
566
  };
541
567
  }
542
- else if ((runnable.type === "path" || runnable.type === "runnableByPath") &&
543
- runnable.path) {
544
- const runType = runnable.runType ?? "script";
568
+ else if (runnable.type === "path" && runnable.runType && runnable.path) {
569
+ // Path-based runnables have type: "path" and runType: "script"|"hubscript"|"flow"
570
+ const prefix = runnable.runType;
545
571
  requestBody.path =
546
- runType !== "hubscript"
547
- ? `${runType}/${runnable.path}`
572
+ prefix !== "hubscript"
573
+ ? `${prefix}/${runnable.path}`
548
574
  : `script/${runnable.path}`;
549
575
  }
550
576
  const uuid = await wmill.executeComponent({
@@ -0,0 +1,159 @@
1
+ // deno-lint-ignore-file no-explicit-any
2
+ import * as dntShim from "../../../_dnt.shims.js";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import process from "node:process";
6
+ import { Command, colors, log, yamlParseFile } from "../../../deps.js";
7
+ import { createBundle } from "./bundle.js";
8
+ import { APP_BACKEND_FOLDER } from "./app_metadata.js";
9
+ import { loadRunnablesFromBackend } from "./raw_apps.js";
10
+ /**
11
+ * Validates the structure of raw_app.yaml
12
+ */
13
+ function validateRawAppYaml(appData) {
14
+ const errors = [];
15
+ const warnings = [];
16
+ // Check required fields
17
+ if (!appData.summary) {
18
+ errors.push("Missing required field: 'summary'");
19
+ }
20
+ else if (typeof appData.summary !== "string") {
21
+ errors.push("Field 'summary' must be a string");
22
+ }
23
+ // Note: 'runnables' is no longer required in raw_app.yaml
24
+ // Runnables can be stored in separate files in the backend folder
25
+ return { errors, warnings };
26
+ }
27
+ /**
28
+ * Validates that runnables exist either in backend/*.yaml files or in raw_app.yaml
29
+ */
30
+ async function validateRunnables(appDir, appData) {
31
+ const errors = [];
32
+ const warnings = [];
33
+ const backendPath = path.join(appDir, APP_BACKEND_FOLDER);
34
+ // Load runnables from separate files (new format)
35
+ const runnablesFromBackend = await loadRunnablesFromBackend(backendPath);
36
+ const hasBackendRunnables = Object.keys(runnablesFromBackend).length > 0;
37
+ // Check for runnables in raw_app.yaml (old format)
38
+ const hasYamlRunnables = appData.runnables &&
39
+ typeof appData.runnables === "object" &&
40
+ !Array.isArray(appData.runnables) &&
41
+ Object.keys(appData.runnables).length > 0;
42
+ if (!hasBackendRunnables && !hasYamlRunnables) {
43
+ errors.push("No runnables found. Expected either:\n" +
44
+ " - Runnable YAML files in the 'backend/' folder (e.g., backend/myRunnable.yaml)\n" +
45
+ " - Or a 'runnables' field in raw_app.yaml (legacy format)");
46
+ }
47
+ else if (hasBackendRunnables) {
48
+ log.info(colors.gray(` Found ${Object.keys(runnablesFromBackend).length} runnable(s) in backend folder`));
49
+ }
50
+ else if (hasYamlRunnables) {
51
+ log.info(colors.gray(` Found ${Object.keys(appData.runnables).length} runnable(s) in raw_app.yaml (legacy format)`));
52
+ warnings.push("Using legacy format with runnables in raw_app.yaml. Consider migrating to separate files in backend/");
53
+ }
54
+ return { errors, warnings };
55
+ }
56
+ /**
57
+ * Checks if the app can be built successfully
58
+ */
59
+ async function validateBuild(appDir) {
60
+ const errors = [];
61
+ const warnings = [];
62
+ try {
63
+ log.info(colors.blue("🔨 Testing build..."));
64
+ // Try to create a bundle - this will validate that all dependencies are in place
65
+ await createBundle({
66
+ production: true,
67
+ minify: false,
68
+ });
69
+ log.info(colors.green("✅ Build successful"));
70
+ }
71
+ catch (error) {
72
+ errors.push(`Build failed: ${error.message}`);
73
+ }
74
+ return { errors, warnings };
75
+ }
76
+ /**
77
+ * Validates a raw app folder
78
+ */
79
+ async function lintRawApp(appDir, opts) {
80
+ const errors = [];
81
+ const warnings = [];
82
+ // Check if we're in a .raw_app folder
83
+ const currentDirName = path.basename(appDir);
84
+ if (!currentDirName.endsWith(".raw_app")) {
85
+ errors.push(`Not a raw app folder: '${currentDirName}' does not end with '.raw_app'`);
86
+ return { valid: false, errors, warnings };
87
+ }
88
+ // Check if raw_app.yaml exists
89
+ const rawAppPath = path.join(appDir, "raw_app.yaml");
90
+ if (!fs.existsSync(rawAppPath)) {
91
+ errors.push("Missing raw_app.yaml file");
92
+ return { valid: false, errors, warnings };
93
+ }
94
+ log.info(colors.blue("📋 Validating raw_app.yaml structure..."));
95
+ // Parse and validate raw_app.yaml
96
+ let appData;
97
+ try {
98
+ appData = await yamlParseFile(rawAppPath);
99
+ }
100
+ catch (error) {
101
+ errors.push(`Failed to parse raw_app.yaml: ${error.message}`);
102
+ return { valid: false, errors, warnings };
103
+ }
104
+ const yamlValidation = validateRawAppYaml(appData);
105
+ errors.push(...yamlValidation.errors);
106
+ warnings.push(...yamlValidation.warnings);
107
+ if (errors.length > 0) {
108
+ return { valid: false, errors, warnings };
109
+ }
110
+ log.info(colors.green("✅ raw_app.yaml structure is valid"));
111
+ // Validate runnables (either in backend folder or in raw_app.yaml)
112
+ log.info(colors.blue("📋 Validating runnables..."));
113
+ const runnablesValidation = await validateRunnables(appDir, appData);
114
+ errors.push(...runnablesValidation.errors);
115
+ warnings.push(...runnablesValidation.warnings);
116
+ if (errors.length > 0) {
117
+ return { valid: false, errors, warnings };
118
+ }
119
+ log.info(colors.green("✅ Runnables are valid"));
120
+ // Validate build
121
+ const buildValidation = await validateBuild(appDir);
122
+ errors.push(...buildValidation.errors);
123
+ warnings.push(...buildValidation.warnings);
124
+ return {
125
+ valid: errors.length === 0,
126
+ errors,
127
+ warnings,
128
+ };
129
+ }
130
+ /**
131
+ * Main lint command
132
+ */
133
+ async function lint(opts, appFolder) {
134
+ const targetDir = appFolder ?? process.cwd();
135
+ log.info(colors.bold.blue(`\n🔍 Linting raw app: ${targetDir}\n`));
136
+ const result = await lintRawApp(targetDir, opts);
137
+ // Display results
138
+ if (result.warnings.length > 0) {
139
+ log.info(colors.yellow("\n⚠️ Warnings:"));
140
+ result.warnings.forEach((warning) => {
141
+ log.info(colors.yellow(` - ${warning}`));
142
+ });
143
+ }
144
+ if (result.errors.length > 0) {
145
+ log.info(colors.red("\n❌ Errors:"));
146
+ result.errors.forEach((error) => {
147
+ log.info(colors.red(` - ${error}`));
148
+ });
149
+ log.info(colors.red("\n❌ Lint failed\n"));
150
+ dntShim.Deno.exit(1);
151
+ }
152
+ log.info(colors.green("\n✅ All checks passed\n"));
153
+ }
154
+ const command = new Command()
155
+ .description("Lint a raw app folder to validate structure and buildability")
156
+ .arguments("[app_folder:string]")
157
+ .option("--fix", "Attempt to fix common issues (not implemented yet)")
158
+ .action(lint);
159
+ export default command;
@@ -2,12 +2,147 @@
2
2
  import * as dntShim from "../../../_dnt.shims.js";
3
3
  import { requireLogin } from "../../core/auth.js";
4
4
  import { resolveWorkspace, validatePath } from "../../core/context.js";
5
- import { colors, log, SEP, windmillUtils, yamlParseFile, } from "../../../deps.js";
5
+ import { colors, log, SEP, windmillUtils, yamlParseFile, yamlStringify, } from "../../../deps.js";
6
6
  import * as wmill from "../../../gen/services.gen.js";
7
+ import path from "node:path";
7
8
  import { isSuperset } from "../../types.js";
8
9
  import { replaceInlineScripts, repopulateFields } from "./apps.js";
9
10
  import { createBundle, detectFrameworks } from "./bundle.js";
10
11
  import { APP_BACKEND_FOLDER } from "./app_metadata.js";
12
+ import { writeIfChanged } from "../../utils/utils.js";
13
+ import { yamlOptions } from "../sync/sync.js";
14
+ import { EXTENSION_TO_LANGUAGE, getLanguageFromExtension, } from "../../../windmill-utils-internal/src/path-utils/path-assigner.js";
15
+ /**
16
+ * Finds the content file for a runnable by looking for files matching the runnableId.
17
+ * Returns the file extension and content, or undefined if not found.
18
+ */
19
+ async function findRunnableContentFile(backendPath, runnableId, allFiles) {
20
+ // Look for files matching pattern: {runnableId}.{ext}
21
+ // where ext is a known language extension
22
+ for (const fileName of allFiles) {
23
+ // Skip yaml and lock files
24
+ if (fileName.endsWith(".yaml") || fileName.endsWith(".lock")) {
25
+ continue;
26
+ }
27
+ // Check if file starts with runnableId followed by a dot
28
+ if (!fileName.startsWith(runnableId + ".")) {
29
+ continue;
30
+ }
31
+ // Extract extension (everything after the first dot following runnableId)
32
+ const ext = fileName.substring(runnableId.length + 1);
33
+ // Check if this is a recognized extension
34
+ if (EXTENSION_TO_LANGUAGE[ext]) {
35
+ try {
36
+ const content = await dntShim.Deno.readTextFile(path.join(backendPath, fileName));
37
+ return { ext, content };
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+ /**
47
+ * Loads all runnables from separate YAML files in the backend folder.
48
+ * Each runnable is stored in a file named `<runnableId>.yaml`.
49
+ *
50
+ * Converts from file format to API format:
51
+ * - For inline scripts (type: 'inline'): derives inlineScript from sibling files
52
+ * - For path-based runnables (type: 'script'|'hubscript'|'flow'): converts to API format
53
+ * e.g., { type: "script" } -> { type: "path", runType: "script" }
54
+ *
55
+ * Returns an empty object if the backend folder doesn't exist.
56
+ *
57
+ * @param backendPath - Path to the backend folder
58
+ * @param defaultTs - Default TypeScript runtime ("bun" or "deno")
59
+ */
60
+ export async function loadRunnablesFromBackend(backendPath, defaultTs = "bun") {
61
+ const runnables = {};
62
+ try {
63
+ // First, collect all files in the backend folder
64
+ const allFiles = [];
65
+ for await (const entry of dntShim.Deno.readDir(backendPath)) {
66
+ if (entry.isFile) {
67
+ allFiles.push(entry.name);
68
+ }
69
+ }
70
+ // Process YAML files (runnable metadata files)
71
+ for (const fileName of allFiles) {
72
+ if (!fileName.endsWith(".yaml")) {
73
+ continue;
74
+ }
75
+ const runnableId = fileName.replace(".yaml", "");
76
+ const filePath = path.join(backendPath, fileName);
77
+ const runnable = (await yamlParseFile(filePath));
78
+ // If this is an inline script (type: 'inline'), derive inlineScript from files
79
+ if (runnable?.type === "inline") {
80
+ const contentFile = await findRunnableContentFile(backendPath, runnableId, allFiles);
81
+ if (contentFile) {
82
+ const language = getLanguageFromExtension(contentFile.ext, defaultTs);
83
+ // Try to load lock file
84
+ let lock;
85
+ try {
86
+ lock = await dntShim.Deno.readTextFile(path.join(backendPath, `${runnableId}.lock`));
87
+ }
88
+ catch {
89
+ // No lock file, that's fine
90
+ }
91
+ // Reconstruct inlineScript object
92
+ runnable.inlineScript = {
93
+ content: contentFile.content,
94
+ language,
95
+ ...(lock ? { lock } : {}),
96
+ };
97
+ }
98
+ }
99
+ else if (runnable?.type === "script" ||
100
+ runnable?.type === "hubscript" ||
101
+ runnable?.type === "flow") {
102
+ // For path-based runnables, convert from file format to API format
103
+ // { type: "script" } -> { type: "path", runType: "script" }
104
+ // { type: "hubscript" } -> { type: "path", runType: "hubscript" }
105
+ // { type: "flow" } -> { type: "path", runType: "flow" }
106
+ const { type, schema: _schema, ...rest } = runnable;
107
+ runnable.type = "path";
108
+ runnable.runType = type;
109
+ // Remove schema if present
110
+ delete runnable.schema;
111
+ Object.assign(runnable, rest);
112
+ }
113
+ runnables[runnableId] = runnable;
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (error.name !== "NotFound") {
118
+ throw error;
119
+ }
120
+ }
121
+ return runnables;
122
+ }
123
+ /**
124
+ * Writes a single runnable to its YAML file in the backend folder.
125
+ * The file will be named `<runnableId>.yaml`.
126
+ *
127
+ * Converts from API format to file format:
128
+ * - For inline scripts: keeps type: "inline"
129
+ * - For path-based runnables: converts { type: "path", runType: "script" } to { type: "script" }
130
+ * and removes schema field
131
+ */
132
+ export function writeRunnableToBackend(backendPath, runnableId, runnable) {
133
+ let runnableToWrite = { ...runnable };
134
+ // Convert path-based runnables from API format to file format
135
+ if (runnable.type === "path" && runnable.runType) {
136
+ // { type: "path", runType: "script" } -> { type: "script" }
137
+ const { type: _type, runType, schema: _schema, ...rest } = runnable;
138
+ runnableToWrite = {
139
+ type: runType,
140
+ ...rest,
141
+ };
142
+ }
143
+ const filePath = path.join(backendPath, `${runnableId}.yaml`);
144
+ writeIfChanged(filePath, yamlStringify(runnableToWrite, yamlOptions));
145
+ }
11
146
  const alreadySynced = [];
12
147
  async function collectAppFiles(localPath) {
13
148
  const files = {};
@@ -27,9 +162,9 @@ async function collectAppFiles(localPath) {
27
162
  }
28
163
  else if (entry.isFile) {
29
164
  // Skip raw_app.yaml as it's metadata, not an app file
30
- // Skip node_modules and package-lock.json as they are generated
31
- if (relativePath === "raw_app.yaml" ||
32
- relativePath === "package-lock.json") {
165
+ // Skip package-lock.json as it's generated
166
+ if (entry.name === "raw_app.yaml" ||
167
+ entry.name === "package-lock.json") {
33
168
  continue;
34
169
  }
35
170
  const content = await dntShim.Deno.readTextFile(fullPath);
@@ -67,11 +202,31 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
67
202
  if (!localPath.endsWith(SEP)) {
68
203
  localPath += SEP;
69
204
  }
70
- const path = localPath + "raw_app.yaml";
71
- const localApp = (await yamlParseFile(path));
72
- replaceInlineScripts(localApp.runnables, localPath + SEP + APP_BACKEND_FOLDER + SEP, true);
73
- repopulateFields(localApp.runnables);
74
- await generatingPolicy(localApp, remotePath, localApp?.["public"] ?? false);
205
+ const appFilePath = localPath + "raw_app.yaml";
206
+ const localApp = (await yamlParseFile(appFilePath));
207
+ // Load runnables from separate YAML files in the backend folder
208
+ // Falls back to reading from raw_app.yaml if no separate files exist (backward compat)
209
+ const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
210
+ const runnablesFromBackend = await loadRunnablesFromBackend(backendPath);
211
+ let runnables;
212
+ if (Object.keys(runnablesFromBackend).length > 0) {
213
+ // Use runnables from separate files (new format)
214
+ runnables = runnablesFromBackend;
215
+ log.info(colors.gray(`Loaded ${Object.keys(runnables).length} runnables from backend folder`));
216
+ }
217
+ else if (localApp.runnables) {
218
+ // Fall back to runnables from raw_app.yaml (old format)
219
+ runnables = localApp.runnables;
220
+ log.info(colors.gray(`Loaded ${Object.keys(runnables).length} runnables from raw_app.yaml (legacy format)`));
221
+ }
222
+ else {
223
+ runnables = {};
224
+ }
225
+ replaceInlineScripts(runnables, backendPath + SEP, true);
226
+ repopulateFields(runnables);
227
+ // Create a temporary app object for policy generation
228
+ const appForPolicy = { ...localApp, runnables };
229
+ await generatingPolicy(appForPolicy, remotePath, localApp?.["public"] ?? false);
75
230
  const files = await collectAppFiles(localPath);
76
231
  async function createBundleRaw() {
77
232
  log.info(colors.yellow.bold(`Creating raw app ${remotePath} bundle...`));
@@ -86,7 +241,7 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
86
241
  });
87
242
  }
88
243
  if (app) {
89
- if (isSuperset(localApp, app)) {
244
+ if (isSuperset({ ...localApp, runnables }, app)) {
90
245
  log.info(colors.green(`App ${remotePath} is up to date`));
91
246
  return;
92
247
  }
@@ -97,10 +252,10 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
97
252
  path: remotePath,
98
253
  formData: {
99
254
  app: {
100
- value: { runnables: localApp.runnables, files },
255
+ value: { runnables, files },
101
256
  path: remotePath,
102
257
  summary: localApp.summary,
103
- policy: localApp.policy,
258
+ policy: appForPolicy.policy,
104
259
  deployment_message: message,
105
260
  custom_path: localApp.custom_path,
106
261
  },
@@ -115,10 +270,10 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
115
270
  workspace,
116
271
  formData: {
117
272
  app: {
118
- value: { runnables: localApp.runnables, files },
273
+ value: { runnables, files },
119
274
  path: remotePath,
120
275
  summary: localApp.summary,
121
- policy: localApp.policy,
276
+ policy: appForPolicy.policy,
122
277
  deployment_message: message,
123
278
  custom_path: localApp.custom_path,
124
279
  },
@@ -126,16 +281,6 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
126
281
  css,
127
282
  },
128
283
  });
129
- // await wmill.createApp({
130
- // workspace,
131
- // requestBody: {
132
- // path: remotePath,
133
- // deployment_message: message,
134
- // value: { runnables: localApp.runnables, files },
135
- // summary: localApp.summary,
136
- // policy: localApp.policy,
137
- // },
138
- // });
139
284
  }
140
285
  }
141
286
  export async function generatingPolicy(app, path, publicApp) {
@@ -4,6 +4,7 @@ import { readLockfile } from "../../utils/metadata.js";
4
4
  import { SCRIPT_GUIDANCE } from "../../guidance/script_guidance.js";
5
5
  import { FLOW_GUIDANCE } from "../../guidance/flow_guidance.js";
6
6
  import { getActiveWorkspaceOrFallback } from "../workspace/workspace.js";
7
+ import { generateRTNamespace } from "../resource-type/resource-type.js";
7
8
  /**
8
9
  * Bootstrap a windmill project with a wmill.yaml file
9
10
  */
@@ -200,6 +201,13 @@ ${flowGuidanceContent}
200
201
  log.warn(`Could not create guidance files: ${error}`);
201
202
  }
202
203
  }
204
+ // Generate resource type namespace
205
+ try {
206
+ await generateRTNamespace(opts);
207
+ }
208
+ catch (error) {
209
+ log.warn(`Could not pull resource types and generate TypeScript namespace: ${error instanceof Error ? error.message : error}`);
210
+ }
203
211
  }
204
212
  const command = new Command()
205
213
  .description("Bootstrap a windmill project with a wmill.yaml file")
@@ -1,10 +1,15 @@
1
1
  // deno-lint-ignore-file no-explicit-any
2
2
  import * as dntShim from "../../../_dnt.shims.js";
3
+ import { writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
3
6
  import { isSuperset, parseFromFile, removeType, } from "../../types.js";
4
7
  import { requireLogin } from "../../core/auth.js";
5
8
  import { resolveWorkspace } from "../../core/context.js";
6
9
  import { colors, Command, log, Table } from "../../../deps.js";
7
10
  import * as wmill from "../../../gen/services.gen.js";
11
+ import { compileResourceTypeToTsType } from "../../utils/resource_types.js";
12
+ import { capitalize, toCamel } from "../../utils/utils.js";
8
13
  export async function pushResourceType(workspace, remotePath, resource, localResource) {
9
14
  remotePath = removeType(remotePath, "resource-type");
10
15
  try {
@@ -61,7 +66,11 @@ async function list(opts) {
61
66
  .header(["Workspace", "Name", "Schema"])
62
67
  .padding(2)
63
68
  .border(true)
64
- .body(res.map((x) => [x.workspace_id ?? "Global", x.name, JSON.stringify(x.schema, null, 2)]))
69
+ .body(res.map((x) => [
70
+ x.workspace_id ?? "Global",
71
+ x.name,
72
+ JSON.stringify(x.schema, null, 2),
73
+ ]))
65
74
  .render();
66
75
  }
67
76
  else {
@@ -73,6 +82,22 @@ async function list(opts) {
73
82
  .render();
74
83
  }
75
84
  }
85
+ export async function generateRTNamespace(opts) {
86
+ const workspace = await resolveWorkspace(opts);
87
+ await requireLogin(opts);
88
+ const rts = await wmill.listResourceType({
89
+ workspace: workspace.workspaceId,
90
+ });
91
+ let namespaceContent = "declare namespace RT {\n";
92
+ namespaceContent += rts
93
+ .map((resourceType) => {
94
+ return ` type ${toCamel(capitalize(resourceType.name))} = ${compileResourceTypeToTsType(resourceType.schema).replaceAll("\n", "\n ")}`;
95
+ })
96
+ .join("\n\n");
97
+ namespaceContent += "\n}";
98
+ writeFileSync(path.join(process.cwd(), "rt.d.ts"), namespaceContent);
99
+ log.info(colors.green("Created rt.d.ts with resource types namespace (RT) for TypeScript."));
100
+ }
76
101
  const command = new Command()
77
102
  .description("resource type related commands")
78
103
  .action(() => log.info("2 actions available, list and push."))
@@ -81,5 +106,7 @@ const command = new Command()
81
106
  .action(list)
82
107
  .command("push", "push a local resource spec. This overrides any remote versions.")
83
108
  .arguments("<file_path:string> <name:string>")
84
- .action(push);
109
+ .action(push)
110
+ .command("generate-namespace", "Create a TypeScript definition file with the RT namespace generated from the resource types")
111
+ .action(generateRTNamespace);
85
112
  export default command;
@@ -17,6 +17,16 @@ import { mergeConfigWithConfigFile, readConfigFile, } from "../../core/conf.js";
17
17
  import { listSyncCodebases } from "../../utils/codebase.js";
18
18
  import fs from "node:fs";
19
19
  import { execSync } from "node:child_process";
20
+ /**
21
+ * Checks if a path is inside a raw app backend folder.
22
+ * Matches patterns like: .../myApp.raw_app/backend/...
23
+ */
24
+ export function isRawAppBackendPath(filePath) {
25
+ // Normalize path separators for consistent matching
26
+ const normalizedPath = filePath.replaceAll(SEP, "/");
27
+ // Check if path contains pattern: *.raw_app/backend/
28
+ return /\.raw_app\/backend\//.test(normalizedPath);
29
+ }
20
30
  async function push(opts, filePath) {
21
31
  opts = await mergeConfigWithConfigFile(opts);
22
32
  const workspace = await resolveWorkspace(opts);
@@ -80,6 +90,7 @@ export async function handleScriptMetadata(path, workspace, alreadySynced, messa
80
90
  }
81
91
  export async function handleFile(path, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases) {
82
92
  if (!path.includes(".inline_script.") &&
93
+ !isRawAppBackendPath(path) &&
83
94
  exts.some((exts) => path.endsWith(exts))) {
84
95
  if (alreadySynced.includes(path)) {
85
96
  return true;