dazscript-framework 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,8 @@ The **DazScript Framework** is a TypeScript-based framework for writing Daz Stud
14
14
 
15
15
  - TypeScript support with full IntelliSense.
16
16
  - A lightweight `action(...)` entrypoint plus helper methods for building interactive scripts.
17
+ - A generated setup dialog for installing, updating, and removing custom action registrations.
18
+ - Setup generation is fully automated from `action(...)` metadata, including menu path, toolbar, shortcut, description, grouping, icons, and bundle-based setup outputs.
17
19
  - Easy integration with Daz Studio for quick script deployment.
18
20
 
19
21
  ## Installation
@@ -65,9 +67,12 @@ export default defineConfig({
65
67
  outDir: './out',
66
68
  defaultMenuPath: '/MyScripts',
67
69
  appDataPath: 'YourName/my-project',
70
+ bundleName: 'My Project',
68
71
  });
69
72
  ```
70
73
 
74
+ `bundleName` is optional display metadata for generated setup dialogs. If omitted, the dialog falls back to `Setup Scripts`. `dazscript init` now scaffolds it automatically from the project folder name.
75
+
71
76
  Built action outputs now use stable launcher shims by default:
72
77
 
73
78
  - `out/<script>.dsa` is the stable launcher registered with Daz Studio menus, toolbars, and shortcuts
@@ -129,7 +134,7 @@ Common `action(...)` parameters:
129
134
  - `toolbar`: the toolbar name used when the action should appear on a toolbar.
130
135
  - `group`: an optional grouping label used by Daz Studio for related actions.
131
136
  - `description`: a longer description for the action.
132
- - `bundle`: generates installer and uninstaller entries as a package bundle instead of a single action entry.
137
+ - `bundle`: generates an additional setup script next to the action file. Use `true` for `Setup.dsa.ts` or a string for `Setup <bundle>.dsa.ts`.
133
138
 
134
139
  When an action is built, the framework emits two files for it:
135
140
 
@@ -140,6 +145,57 @@ Generated installers register the launcher path, so menu placement, toolbars, sh
140
145
 
141
146
  If the local `lib/` implementation is missing, the launcher falls back to the configured `appDataPath`. Builds now require this value and validate it as a unique `Author/Product` style path.
142
147
 
148
+ ### Generated Setup Script
149
+
150
+ Running `npm run installer` generates `src/Setup.dsa.ts` for the project.
151
+
152
+ This flow is completely automated. The installer generator scans runnable `.dsa.ts` entry files, reads the top-level `action(...)` call, and derives the setup dialog rows and registration behavior directly from that metadata. In practice, the menu path, toolbar target, shortcut, description, grouping, icon usage, and bundle-specific setup outputs all come from the action definition rather than from separate installer code you have to maintain by hand.
153
+
154
+ The generated setup script:
155
+
156
+ - Scans all runnable top-level `.dsa.ts` files under `scriptsPath`
157
+ - Reads `action(...)` metadata directly from the source
158
+ - Normalizes default menu paths relative to `defaultMenuPath`
159
+ - Derives action labels, descriptions, shortcuts, toolbar targets, grouping, and icons from the action definition
160
+ - Writes one searchable setup entry per discovered action
161
+ - Uses `appDataPath/Installer` as the installer settings namespace
162
+ - Passes `bundleName` through so the dialog title can be project-specific
163
+
164
+ The setup dialog initializes from the current Daz Studio install state rather than assuming a clean install. It checks which actions are already installed, which ones are present in menus or toolbars, and what shortcut is currently assigned.
165
+
166
+ Current setup dialog behavior:
167
+
168
+ - Shows an install checkbox plus the columns `Action`, `Shortcut`, `Description`, `Menu`, and `Toolbar`
169
+ - Includes a search box that filters by action name, shortcut, description, menu path, and toolbar
170
+ - Supports `Select All` and `Deselect All` for the currently visible rows
171
+ - Lets the user right-click an action to set a shortcut or reset it to the default shortcut
172
+ - Shows shortcut overrides with an `[ovr]` marker
173
+ - Displays the configured toolbar name directly instead of a generic yes/no flag
174
+ - Uses the configured `bundleName` in the window title when available
175
+
176
+ Applying the setup dialog does both install and cleanup work:
177
+
178
+ - Selected rows are installed or updated through the framework custom action helpers
179
+ - Unselected rows are removed from supported menu and toolbar targets
180
+ - Affected toolbars are rebuilt after removal so remaining selected actions stay grouped correctly
181
+ - Empty toolbars created by the framework are cleaned up automatically
182
+
183
+ The generated project-level setup file replaces the older generated `Install.dsa.ts` and `Uninstall.dsa.ts` flow. The installer generator now removes those legacy files if they still exist.
184
+
185
+ ### Action-Level Bundles
186
+
187
+ The `bundle` property on `action(...)` is separate from project `bundleName`.
188
+
189
+ - `bundleName` in `dazscript.config.ts` is project metadata used for the setup dialog title
190
+ - `bundle` in an action definition changes installer generation behavior for that action
191
+
192
+ When `bundle` is set on an action, the installer generator also writes a setup script beside that action:
193
+
194
+ - `bundle: true` writes `Setup.dsa.ts`
195
+ - `bundle: 'Utilities'` writes `Setup Utilities.dsa.ts`
196
+
197
+ Those bundle-generated setup files use the same setup dialog helper and now also receive the project `bundleName`.
198
+
143
199
  ### Building UIs with Observables & Dialogs
144
200
 
145
201
  The framework uses a **Model-View pattern** with reactive data bindings:
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ function toPosix(filePath) {
4
+ return filePath.replace(/\\/g, '/');
5
+ }
6
+
7
+ function validateAppDataPath(appDataPath, workdir) {
8
+ if (!appDataPath || typeof appDataPath !== 'string') {
9
+ throw new Error(
10
+ `[dazscript] Missing required appDataPath in ${workdir}. ` +
11
+ `Set appDataPath: 'Author/Product' in dazscript.config.ts.`
12
+ );
13
+ }
14
+
15
+ const normalized = toPosix(appDataPath).trim().replace(/^\/+|\/+$/g, '');
16
+ const segments = normalized.split('/').filter(Boolean);
17
+
18
+ if (segments.length < 2) {
19
+ throw new Error(
20
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
21
+ `Use at least two segments, for example 'Author/Product'.`
22
+ );
23
+ }
24
+
25
+ const blockedSegments = new Set([
26
+ 'appdata',
27
+ 'cache',
28
+ 'data',
29
+ 'lib',
30
+ 'libs',
31
+ 'script',
32
+ 'scripts',
33
+ 'temp',
34
+ 'tmp',
35
+ ]);
36
+
37
+ const invalidSegment = segments.find((segment) => blockedSegments.has(segment.toLowerCase()));
38
+ if (invalidSegment) {
39
+ throw new Error(
40
+ `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
41
+ `Path segments like "${invalidSegment}" are too generic. Use a unique Author/Product path.`
42
+ );
43
+ }
44
+
45
+ return normalized;
46
+ }
47
+
48
+ module.exports = {
49
+ validateAppDataPath,
50
+ };
@@ -50,10 +50,19 @@ export default defineConfig({
50
50
  outDir: '${options.outDir}',
51
51
  defaultMenuPath: '${options.menuPath}',
52
52
  appDataPath: '${options.appDataPath}',
53
+ bundleName: '${options.bundleName}',
53
54
  });
54
55
  `;
55
56
  }
56
57
 
58
+ function toBundleName(projectName) {
59
+ return projectName
60
+ .split(/[-_\s]+/)
61
+ .filter(Boolean)
62
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
63
+ .join(' ');
64
+ }
65
+
57
66
  function buildTsconfigContent() {
58
67
  return `{
59
68
  "extends": "./node_modules/dazscript-framework/tsconfig.json",
@@ -102,6 +111,7 @@ function initProject(workdir, rawOptions) {
102
111
  scriptsPath: normalizePath(rawOptions.scriptsPath, './src'),
103
112
  outDir: normalizePath(rawOptions.outDir, './out'),
104
113
  appDataPath: normalizePath(rawOptions.appDataPath, `YourName/${projectName}`),
114
+ bundleName: toBundleName(projectName),
105
115
  };
106
116
 
107
117
  writeFileIfNeeded(
@@ -2,6 +2,8 @@ const ts = require('typescript');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const glob = require('glob');
5
+ const { loadConfig } = require('./config-loader');
6
+ const { validateAppDataPath } = require('./app-data-path');
5
7
 
6
8
  const nameofActionFunction = 'action';
7
9
 
@@ -19,19 +21,14 @@ function stringOrDefault(str, defaultValue) {
19
21
  return str !== undefined && str !== null && str !== '' ? str : defaultValue;
20
22
  }
21
23
 
22
- function generateInstallerTemplate(data) {
24
+ function generateInstallerTemplate(data, settingsPath, bundleName) {
23
25
  return `
24
- import { installCustomActions as install } from '@dsf/helpers/custom-action-helper';
26
+ import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
25
27
 
26
- install(${data});
27
- `;
28
- }
29
-
30
- function generateUninstallerTemplate(data) {
31
- return `
32
- import { uninstallCustomActions as uninstall } from '@dsf/helpers/custom-action-helper';
33
-
34
- uninstall(${data});
28
+ setup(${data}, ${JSON.stringify({
29
+ settingsPath,
30
+ bundleName,
31
+ })});
35
32
  `;
36
33
  }
37
34
 
@@ -154,7 +151,7 @@ function findActionEntryFiles(workdir, options) {
154
151
  });
155
152
  }
156
153
 
157
- function processScript(filePath, container, defaultMenuPath) {
154
+ function processScript(filePath, container, defaultMenuPath, settingsPath, bundleName) {
158
155
  const fileInfo = path.parse(filePath);
159
156
  const content = fs.readFileSync(filePath, 'utf-8').toString();
160
157
  const actionCall = findTopLevelActionCall(content, filePath);
@@ -208,38 +205,29 @@ function processScript(filePath, container, defaultMenuPath) {
208
205
  container.scripts.push(script);
209
206
 
210
207
  if (decorator.bundle !== undefined) {
211
- let packageInstallerFilePath = `Install.dsa.ts`;
212
- let packageUninstallerFilePath = `Uninstall.dsa.ts`;
208
+ let packageSetupFilePath = `Setup.dsa.ts`;
213
209
 
214
210
  if (decorator.bundle !== true) {
215
- packageInstallerFilePath = `Install ${decorator.bundle}.dsa.ts`;
216
- packageUninstallerFilePath = `Uninstall ${decorator.bundle}.dsa.ts`;
211
+ packageSetupFilePath = `Setup ${decorator.bundle}.dsa.ts`;
217
212
  }
218
213
 
219
214
  let bundleScriptContent = generateInstallerTemplate(
220
- JSON.stringify(container.scripts, null, 4)
215
+ JSON.stringify(container.scripts, null, 4),
216
+ settingsPath,
217
+ bundleName
221
218
  );
222
219
  let bundleScriptFilePath = path.join(
223
220
  path.parse(filePath).dir,
224
- packageInstallerFilePath
225
- );
226
- fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
227
-
228
- bundleScriptContent = generateUninstallerTemplate(
229
- JSON.stringify(container.scripts, null, 4)
230
- );
231
- bundleScriptFilePath = path.join(
232
- path.parse(filePath).dir,
233
- packageUninstallerFilePath
221
+ packageSetupFilePath
234
222
  );
235
223
  fs.writeFileSync(bundleScriptFilePath, bundleScriptContent);
236
224
  }
237
225
  }
238
226
 
239
- function processScripts(paths, container, defaultMenuPath) {
227
+ function processScripts(paths, container, defaultMenuPath, settingsPath, bundleName) {
240
228
  paths.forEach((filePath) => {
241
229
  console.log(`Processing ${filePath}`);
242
- processScript(filePath, container, defaultMenuPath);
230
+ processScript(filePath, container, defaultMenuPath, settingsPath, bundleName);
243
231
  });
244
232
  }
245
233
 
@@ -247,10 +235,16 @@ function generateInstallerFiles(workdir, options) {
247
235
  const defaultMenuPath = options.defaultMenuPath.endsWith('/')
248
236
  ? options.defaultMenuPath
249
237
  : `${options.defaultMenuPath}/`;
238
+ const { config } = loadConfig(workdir);
239
+ const appDataPath = validateAppDataPath(options.appDataPath || config.appDataPath, workdir);
240
+ const settingsPath = `${appDataPath}/Installer`;
241
+ const bundleName = typeof config.bundleName === 'string' && config.bundleName.trim()
242
+ ? config.bundleName.trim()
243
+ : undefined;
250
244
  const container = { scripts: [] };
251
245
  const matches = findActionEntryFiles(workdir, options);
252
246
 
253
- processScripts(matches, container, defaultMenuPath);
247
+ processScripts(matches, container, defaultMenuPath, settingsPath, bundleName);
254
248
 
255
249
  container.scripts = container.scripts.sort((a, b) => {
256
250
  const aKey = a.menuPath + a.filePath;
@@ -259,17 +253,16 @@ function generateInstallerFiles(workdir, options) {
259
253
  });
260
254
 
261
255
  const installerScriptContent = generateInstallerTemplate(
262
- JSON.stringify(container.scripts, null, 4)
256
+ JSON.stringify(container.scripts, null, 4),
257
+ settingsPath,
258
+ bundleName
263
259
  );
264
- fs.writeFileSync(path.join(workdir, 'src', 'Install.dsa.ts'), installerScriptContent);
260
+ fs.writeFileSync(path.join(workdir, 'src', 'Setup.dsa.ts'), installerScriptContent);
265
261
 
266
- const uninstallerScriptContent = generateUninstallerTemplate(
267
- JSON.stringify(container.scripts, null, 4)
268
- );
269
- fs.writeFileSync(
270
- path.join(workdir, 'src', 'Uninstall.dsa.ts'),
271
- uninstallerScriptContent
272
- );
262
+ const installPath = path.join(workdir, 'src', 'Install.dsa.ts');
263
+ const uninstallPath = path.join(workdir, 'src', 'Uninstall.dsa.ts');
264
+ if (fs.existsSync(installPath)) fs.unlinkSync(installPath);
265
+ if (fs.existsSync(uninstallPath)) fs.unlinkSync(uninstallPath);
273
266
  }
274
267
 
275
268
  if (require.main === module) {
@@ -277,6 +270,7 @@ if (require.main === module) {
277
270
  const options = {
278
271
  scriptsPath: './src',
279
272
  defaultMenuPath: 'My Scripts',
273
+ appDataPath: undefined,
280
274
  };
281
275
 
282
276
  for (let index = 0; index < args.length; index += 1) {
@@ -294,6 +288,12 @@ if (require.main === module) {
294
288
  continue;
295
289
  }
296
290
 
291
+ if (arg === '--app-data-path') {
292
+ options.appDataPath = args[index + 1];
293
+ index += 1;
294
+ continue;
295
+ }
296
+
297
297
  throw new Error(`Unknown option: ${arg}`);
298
298
  }
299
299
 
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { findActionEntryFiles } = require('./install-generator');
6
+ const { validateAppDataPath } = require('./app-data-path');
6
7
 
7
8
  function toPosix(filePath) {
8
9
  return filePath.replace(/\\/g, '/');
@@ -25,47 +26,6 @@ function getImplementationRelativePath(outputRelativePath) {
25
26
  return path.posix.join(implementationDirectory, 'script.dsa');
26
27
  }
27
28
 
28
- function validateAppDataPath(appDataPath, workdir) {
29
- if (!appDataPath || typeof appDataPath !== 'string') {
30
- throw new Error(
31
- `[dazscript] Missing required appDataPath in ${workdir}. ` +
32
- `Set appDataPath: 'Author/Product' in dazscript.config.ts.`
33
- );
34
- }
35
-
36
- const normalized = toPosix(appDataPath).trim().replace(/^\/+|\/+$/g, '');
37
- const segments = normalized.split('/').filter(Boolean);
38
-
39
- if (segments.length < 2) {
40
- throw new Error(
41
- `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
42
- `Use at least two segments, for example 'Author/Product'.`
43
- );
44
- }
45
-
46
- const blockedSegments = new Set([
47
- 'appdata',
48
- 'cache',
49
- 'data',
50
- 'lib',
51
- 'libs',
52
- 'script',
53
- 'scripts',
54
- 'temp',
55
- 'tmp',
56
- ]);
57
-
58
- const invalidSegment = segments.find((segment) => blockedSegments.has(segment.toLowerCase()));
59
- if (invalidSegment) {
60
- throw new Error(
61
- `[dazscript] Invalid appDataPath "${appDataPath}" in ${workdir}. ` +
62
- `Path segments like "${invalidSegment}" are too generic. Use a unique Author/Product path.`
63
- );
64
- }
65
-
66
- return normalized;
67
- }
68
-
69
29
  function makeLauncherSource(implementationRelativePath, appDataImplementationRelativePath) {
70
30
  return [
71
31
  '// Auto-generated launcher shim.',
@@ -158,5 +118,4 @@ function createActionLaunchers(workdir, options) {
158
118
 
159
119
  module.exports = {
160
120
  createActionLaunchers,
161
- validateAppDataPath,
162
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -67,6 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^22.10.5",
70
+ "dazscript-types": "^0.2.4",
70
71
  "eslint": "^9.17.0",
71
72
  "typescript": "^5.7.3"
72
73
  }
@@ -1,7 +1,7 @@
1
1
 
2
- import { installCustomActions as install } from '@dsf/helpers/custom-action-helper';
2
+ import { showSetupCustomActionsDialog as setup } from '@dsf/helpers/custom-action-installer-helper';
3
3
 
4
- install([
4
+ setup([
5
5
  {
6
6
  "name": null,
7
7
  "text": "Hello World",
@@ -16,4 +16,4 @@ install([
16
16
  "menuPath": "/DazScriptFramework/samples",
17
17
  "description": "sample-dialog"
18
18
  }
19
- ]);
19
+ ], {"settingsPath":"DazScriptFramework/samples/Installer","bundleName":"Samples"});
@@ -37,7 +37,7 @@ const shortcutTokenMap: { [key: string]: string } = {
37
37
  'DOWN': 'Down',
38
38
  }
39
39
 
40
- const normalizeShortcut = (shortcut: string): string => {
40
+ export const normalizeShortcut = (shortcut: string): string => {
41
41
  if (!shortcut) return ''
42
42
 
43
43
  return shortcut
@@ -53,6 +53,19 @@ const normalizeShortcut = (shortcut: string): string => {
53
53
  .join('+')
54
54
  }
55
55
 
56
+ export const getActionShortcut = (name: string): string => {
57
+ const action = findAction(name)
58
+ if (!action) return ''
59
+
60
+ if (isCustomAction(action)) {
61
+ const index = actionMgr.findCustomAction(name)
62
+ if (index < 0) return ''
63
+ return normalizeShortcut(String(actionMgr.getCustomActionShortcut(index) ?? '').trim())
64
+ }
65
+
66
+ return normalizeShortcut(String(action.shortcut?.toString() ?? '').trim())
67
+ }
68
+
56
69
  type ActionShortcutEntry = {
57
70
  action: DzAction
58
71
  shortcut: string