@dimescheduler/setup 0.1.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.
package/README.md ADDED
@@ -0,0 +1,404 @@
1
+ # @dimescheduler/setup
2
+
3
+ CLI tool to validate, compile, and deploy Dime Scheduler configurations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @dimescheduler/setup
9
+ ```
10
+
11
+ Or use with npx:
12
+
13
+ ```bash
14
+ npx @dimescheduler/setup <command>
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### `validate`
20
+
21
+ Validate a configuration file against the Dime Scheduler DSL schema.
22
+
23
+ ```bash
24
+ dimescheduler-setup validate <file>
25
+ ```
26
+
27
+ **Arguments:**
28
+ - `<file>` - Path to the configuration file (.json5 or .json)
29
+
30
+ **Options:**
31
+ - `--json` - Output results as JSON
32
+
33
+ **Examples:**
34
+
35
+ ```bash
36
+ # Validate a configuration file
37
+ dimescheduler-setup validate my-profile.json5
38
+
39
+ # Get validation results as JSON (useful for CI/CD)
40
+ dimescheduler-setup validate my-profile.json5 --json
41
+ ```
42
+
43
+ **Exit codes:**
44
+ - `0` - Configuration is valid
45
+ - `1` - Configuration has errors
46
+
47
+ ---
48
+
49
+ ### `compile`
50
+
51
+ Compile a JSON5 configuration to API-ready JSON format.
52
+
53
+ ```bash
54
+ dimescheduler-setup compile <file> [options]
55
+ ```
56
+
57
+ **Arguments:**
58
+ - `<file>` - Path to the configuration file (.json5)
59
+
60
+ **Options:**
61
+ - `-o, --output <dir>` - Output directory (default: same as input file)
62
+ - `--stdout` - Output to stdout instead of writing to a file
63
+ - `--skip-validation` - Skip validation before compiling
64
+
65
+ **Examples:**
66
+
67
+ ```bash
68
+ # Compile to JSON (creates my-profile.json in the same directory)
69
+ dimescheduler-setup compile my-profile.json5
70
+
71
+ # Compile to a specific directory
72
+ dimescheduler-setup compile my-profile.json5 -o ./dist
73
+
74
+ # Output to stdout (useful for piping)
75
+ dimescheduler-setup compile my-profile.json5 --stdout
76
+
77
+ # Skip validation (not recommended)
78
+ dimescheduler-setup compile my-profile.json5 --skip-validation
79
+ ```
80
+
81
+ ---
82
+
83
+ ### `deploy`
84
+
85
+ Deploy a configuration directly to a Dime Scheduler instance.
86
+
87
+ ```bash
88
+ dimescheduler-setup deploy <file> --api-key <key> --env <environment> [options]
89
+ ```
90
+
91
+ **Arguments:**
92
+ - `<file>` - Path to the configuration file (.json5 or .json)
93
+
94
+ **Options:**
95
+ - `--api-key <key>` - API key for authentication (required, or set `DS_API_KEY` env var)
96
+ - `--env <environment>` - Environment: `production`, `sandbox`, or `test` (required)
97
+ - `--skip-validation` - Skip validation before deploying
98
+ - `--json` - Output results as JSON
99
+ - `--dry-run` - Validate and compile without actually deploying
100
+ - `-y, --yes` - Skip confirmation prompt
101
+
102
+ **Examples:**
103
+
104
+ ```bash
105
+ # Deploy to sandbox (shows confirmation prompt)
106
+ dimescheduler-setup deploy my-profile.json5 --api-key YOUR_API_KEY --env sandbox
107
+
108
+ # Deploy to production with confirmation
109
+ dimescheduler-setup deploy my-profile.json5 --api-key YOUR_API_KEY --env production
110
+
111
+ # Skip confirmation prompt (for CI/CD)
112
+ dimescheduler-setup deploy my-profile.json5 --api-key YOUR_API_KEY --env sandbox -y
113
+
114
+ # Use environment variable for API key
115
+ export DS_API_KEY=YOUR_API_KEY
116
+ dimescheduler-setup deploy my-profile.json5 --env sandbox
117
+
118
+ # Dry run (validate and compile only)
119
+ dimescheduler-setup deploy my-profile.json5 --api-key YOUR_API_KEY --env sandbox --dry-run
120
+
121
+ # Get results as JSON (useful for CI/CD, skips confirmation)
122
+ dimescheduler-setup deploy my-profile.json5 --api-key YOUR_API_KEY --env sandbox --json
123
+ ```
124
+
125
+ **Exit codes:**
126
+ - `0` - Deployment successful (or cancelled by user)
127
+ - `1` - Validation or compilation error
128
+ - `2` - Deployment failed (API error)
129
+
130
+ ## Configuration Format
131
+
132
+ Configuration files use [JSON5](https://json5.org/) format, which supports:
133
+ - Comments (`//` and `/* */`)
134
+ - Trailing commas
135
+ - Unquoted keys
136
+ - Single-quoted strings
137
+
138
+ ### Example Configuration
139
+
140
+ ```json5
141
+ {
142
+ name: "Service Planning",
143
+ code: "SERVICE_PLANNING",
144
+
145
+ // Optional metadata
146
+ owner: "admin@example.com",
147
+ notificationEmail: "notifications@example.com",
148
+
149
+ // Visual theme
150
+ theme: {
151
+ color: "blue",
152
+ scheme: "sltl"
153
+ },
154
+
155
+ // Sharing settings
156
+ shared: {
157
+ global: false,
158
+ userGroup: "Planners"
159
+ },
160
+
161
+ // Route calculation settings
162
+ route: {
163
+ profile: "default",
164
+ calculateRoutes: true,
165
+ showSequenceIndicators: true,
166
+ unitOfDistance: "km" // or "mi"
167
+ },
168
+
169
+ // Planning board settings
170
+ planning: {
171
+ snapInterval: { mode: "1hour" }, // or "15min", "30min", "day", or 1-10
172
+ range: { mode: "week" }, // or "day", "month", "year"
173
+ start: { mode: "today" }, // or "yesterday", "week", "month"
174
+ hours: { start: 8, end: 18 }
175
+ },
176
+
177
+ // Workspace layout (GoldenLayout structure)
178
+ workspace: [
179
+ {
180
+ type: "row",
181
+ content: [
182
+ {
183
+ type: "column",
184
+ width: 30,
185
+ content: [
186
+ {
187
+ type: "stack",
188
+ content: [
189
+ {
190
+ type: "component",
191
+ component: "openTasks",
192
+ title: "Open Tasks",
193
+ id: "openTasks1",
194
+ layouts: [{
195
+ id: "openTasks1",
196
+ code: "OPEN_TASKS",
197
+ name: "Default",
198
+ default: true,
199
+ columns: [
200
+ { property: "Job.JobNo" },
201
+ { property: "TaskNo" }
202
+ ],
203
+ sorters: [
204
+ { property: "Job.JobNo", direction: "ASC" }
205
+ ]
206
+ }]
207
+ }
208
+ ]
209
+ }
210
+ ]
211
+ },
212
+ {
213
+ type: "column",
214
+ width: 70,
215
+ content: [
216
+ {
217
+ type: "stack",
218
+ content: [
219
+ {
220
+ type: "component",
221
+ component: "planningBoard",
222
+ title: "Planning Board",
223
+ layouts: [{
224
+ id: "planningBoard1",
225
+ code: "SCHEDULER",
226
+ name: "Default View",
227
+ default: true,
228
+ viewPreset: "week",
229
+ columns: [
230
+ { property: "DisplayName" },
231
+ { property: "Department" }
232
+ ],
233
+ grouper: {
234
+ property: "ResourceType.DisplayName",
235
+ direction: "ASC",
236
+ id: "ResourceType.DisplayName"
237
+ }
238
+ }]
239
+ }
240
+ ]
241
+ }
242
+ ]
243
+ }
244
+ ]
245
+ }
246
+ ],
247
+
248
+ // Optional: User definitions
249
+ users: [
250
+ {
251
+ name: "John Doe",
252
+ email: "john@example.com",
253
+ language: "en",
254
+ timeZone: "America/New_York",
255
+ profiles: [{ name: "SERVICE_PLANNING", default: true }],
256
+ roles: ["Administrator"],
257
+ timeMarkers: ["IN PROCESS"],
258
+ categories: ["HARDWARE"],
259
+ filterValues: ["East"]
260
+ }
261
+ ]
262
+ }
263
+ ```
264
+
265
+ ### Available Components
266
+
267
+ | Component | Description |
268
+ |-----------|-------------|
269
+ | `openTasks` | Grid showing unplanned tasks |
270
+ | `planningBoard` | Main scheduling board |
271
+ | `mapComponent` | Map view |
272
+ | `indicators` | Category indicators |
273
+ | `resourceFilters` | Resource filter panel |
274
+ | `plannedTasksGridComponent` | Grid showing planned tasks |
275
+ | `propertygridComponent` | Details/properties panel |
276
+ | `datePickerComponent` | Calendar date picker |
277
+ | `ganttComponent` | Gantt chart view |
278
+ | `capacityComponent` | Capacity planning view |
279
+ | `routeSequenceComponent` | Route sequence grid |
280
+ | `notificationsComponent` | Notifications panel |
281
+
282
+ ### Layout Configuration
283
+
284
+ Components can include `layouts` for configuring grid columns, sorting, filtering, and grouping:
285
+
286
+ ```json5
287
+ layouts: [{
288
+ id: "myGrid1",
289
+ code: "MY_LAYOUT",
290
+ name: "My Custom Layout",
291
+ default: true,
292
+
293
+ // Column configuration
294
+ columns: [
295
+ { property: "Job.JobNo", width: 100 },
296
+ { property: "Description" }
297
+ ],
298
+
299
+ // Sorting
300
+ sorters: [
301
+ { property: "Job.JobNo", direction: "ASC" },
302
+ { property: "TaskNo", direction: "DESC" }
303
+ ],
304
+
305
+ // Filtering
306
+ filters: [
307
+ { property: "Status", operator: "eq", value: "Active" }
308
+ ],
309
+
310
+ // Grouping
311
+ grouper: {
312
+ property: "Category",
313
+ direction: "ASC",
314
+ id: "Category"
315
+ },
316
+
317
+ // Grid settings
318
+ pageSize: 50,
319
+ rowHeight: "2rows", // "1row", "2rows", "3rows", "4rows", or pixel value
320
+ viewPreset: "week", // "day", "week", "workWeek", "month"
321
+ lockedGridWidth: "30%",
322
+ fitToScreen: false,
323
+
324
+ // Sharing (optional)
325
+ shared: {
326
+ public: true,
327
+ userGroup: "Planners"
328
+ }
329
+ }]
330
+ ```
331
+
332
+ ## CI/CD Integration
333
+
334
+ ### GitHub Actions Example
335
+
336
+ ```yaml
337
+ name: Deploy Configuration
338
+
339
+ on:
340
+ push:
341
+ branches: [main]
342
+ paths:
343
+ - 'configs/*.json5'
344
+
345
+ jobs:
346
+ deploy:
347
+ runs-on: ubuntu-latest
348
+ steps:
349
+ - uses: actions/checkout@v4
350
+
351
+ - uses: actions/setup-node@v4
352
+ with:
353
+ node-version: '20'
354
+
355
+ - name: Install CLI
356
+ run: npm install -g @dimescheduler/setup
357
+
358
+ - name: Validate
359
+ run: dimescheduler-setup validate configs/my-profile.json5
360
+
361
+ - name: Deploy
362
+ run: dimescheduler-setup deploy configs/my-profile.json5 --env sandbox --json
363
+ env:
364
+ DS_API_KEY: ${{ secrets.DS_API_KEY }}
365
+ ```
366
+
367
+ ### Azure DevOps Example
368
+
369
+ ```yaml
370
+ trigger:
371
+ branches:
372
+ include:
373
+ - main
374
+ paths:
375
+ include:
376
+ - configs/*.json5
377
+
378
+ pool:
379
+ vmImage: 'ubuntu-latest'
380
+
381
+ steps:
382
+ - task: NodeTool@0
383
+ inputs:
384
+ versionSpec: '20.x'
385
+
386
+ - script: npm install -g @dimescheduler/setup
387
+ displayName: 'Install CLI'
388
+
389
+ - script: dimescheduler-setup validate configs/my-profile.json5
390
+ displayName: 'Validate Configuration'
391
+
392
+ - script: dimescheduler-setup deploy configs/my-profile.json5 --env sandbox -y
393
+ displayName: 'Deploy Configuration'
394
+ env:
395
+ DS_API_KEY: $(DS_API_KEY)
396
+ ```
397
+
398
+ ## Requirements
399
+
400
+ - Node.js 18 or higher
401
+
402
+ ## License
403
+
404
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,732 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command4 } from "commander";
5
+
6
+ // src/commands/validate.ts
7
+ import { Command } from "commander";
8
+ import { readFileSync } from "fs";
9
+ import { resolve, normalize } from "path";
10
+ import JSON5 from "json5";
11
+ import pc from "picocolors";
12
+
13
+ // src/core/validate.ts
14
+ function countComponents(nodes) {
15
+ let count = 0;
16
+ for (const node of nodes) {
17
+ if (node.type === "component") {
18
+ count++;
19
+ } else if ("content" in node) {
20
+ count += countComponents(node.content);
21
+ }
22
+ }
23
+ return count;
24
+ }
25
+ function hasComponent(nodes, componentType) {
26
+ for (const node of nodes) {
27
+ if (node.type === "component" && node.component === componentType) {
28
+ return true;
29
+ } else if ("content" in node) {
30
+ if (hasComponent(node.content, componentType)) {
31
+ return true;
32
+ }
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ function validateProfile(profile) {
38
+ const issues = [];
39
+ if (!profile.name?.trim()) {
40
+ issues.push({ type: "error", message: "Profile name is required" });
41
+ }
42
+ if (!profile.code?.trim()) {
43
+ issues.push({ type: "error", message: "Profile code is required" });
44
+ }
45
+ if (!profile.workspace || profile.workspace.length === 0) {
46
+ issues.push({ type: "error", message: "Workspace must have at least one element" });
47
+ } else {
48
+ const componentCount = countComponents(profile.workspace);
49
+ if (componentCount === 0) {
50
+ issues.push({ type: "warning", message: "Workspace has no components" });
51
+ }
52
+ const hasPlanningBoard = hasComponent(profile.workspace, "planningBoard");
53
+ if (!hasPlanningBoard) {
54
+ issues.push({ type: "warning", message: "Consider adding a Planning Board component" });
55
+ }
56
+ }
57
+ if (profile.users && profile.users.length > 0) {
58
+ profile.users.forEach((user, index) => {
59
+ if (!user.name?.trim()) {
60
+ issues.push({ type: "error", message: `User ${index + 1}: Name is required` });
61
+ }
62
+ if (!user.email?.trim()) {
63
+ issues.push({ type: "error", message: `User ${index + 1}: Email is required` });
64
+ }
65
+ });
66
+ }
67
+ const errors = issues.filter((i) => i.type === "error");
68
+ const warnings = issues.filter((i) => i.type === "warning");
69
+ return {
70
+ valid: errors.length === 0,
71
+ errors,
72
+ warnings
73
+ };
74
+ }
75
+
76
+ // src/core/compile.ts
77
+ var SNAP_MODE_MAP = {
78
+ "none": 1,
79
+ "off": 1,
80
+ "5min": 2,
81
+ "5minutes": 2,
82
+ "10min": 3,
83
+ "10minutes": 3,
84
+ "15min": 4,
85
+ "15minutes": 4,
86
+ "30min": 5,
87
+ "30minutes": 5,
88
+ "halfhour": 5,
89
+ "1hour": 6,
90
+ "1h": 6,
91
+ "hour": 6,
92
+ "12hours": 7,
93
+ "12h": 7,
94
+ "1day": 8,
95
+ "1d": 8,
96
+ "day": 8,
97
+ "2hours": 9,
98
+ "2h": 9,
99
+ "4hours": 10,
100
+ "4h": 10
101
+ };
102
+ var RANGE_MODE_MAP = {
103
+ "day": 1,
104
+ "days": 1,
105
+ "hours": 1,
106
+ "week": 2,
107
+ "weeks": 2,
108
+ "month": 3,
109
+ "months": 3,
110
+ "custom": 4
111
+ };
112
+ var START_MODE_MAP = {
113
+ "today": 1,
114
+ "absolute": 1,
115
+ "startofweek": 2,
116
+ "relative": 2,
117
+ "startofmonth": 3,
118
+ "todayoffset": 4
119
+ };
120
+ var COMPONENT_TO_GL = {
121
+ "openTasks": "openTasksGridComponent",
122
+ "planningBoard": "schedulerComponent",
123
+ "mapComponent": "mapComponent",
124
+ "indicators": "categoriesComponent",
125
+ "resourceFilters": "filterComponent",
126
+ "plannedTasksGridComponent": "plannedTasksGridComponent",
127
+ "propertygridComponent": "propertygridComponent",
128
+ "datePickerComponent": "datePickerComponent",
129
+ "ganttComponent": "ganttComponent",
130
+ "capacityComponent": "capacityComponent",
131
+ "routeSequenceComponent": "routeSequenceComponent",
132
+ "notificationsComponent": "notificationsComponent"
133
+ };
134
+ var COMPONENT_TO_CONTEXT = {
135
+ "planningBoard": "scheduler",
136
+ "openTasks": "unplannedTasksGrid",
137
+ "plannedTasksGridComponent": "plannedTasksGrid",
138
+ "notificationsComponent": "notificationsGrid",
139
+ "propertygridComponent": "detailsGrid",
140
+ "capacityComponent": "pivotGrid",
141
+ "ganttComponent": "gantt",
142
+ "routeSequenceComponent": "routeSequenceGrid"
143
+ };
144
+ var VIEW_PRESET_MAP = {
145
+ "day": "1",
146
+ "1": "1",
147
+ "week": "2",
148
+ "2": "2",
149
+ "workweek": "3",
150
+ "workWeek": "3",
151
+ "work_week": "3",
152
+ "3": "3",
153
+ "month": "4",
154
+ "4": "4"
155
+ };
156
+ var ROW_HEIGHT_MAP = {
157
+ "1row": 25,
158
+ "1rows": 25,
159
+ "1": 25,
160
+ "25": 25,
161
+ "2rows": 38,
162
+ "2row": 38,
163
+ "2": 38,
164
+ "38": 38,
165
+ "3rows": 50,
166
+ "3row": 50,
167
+ "3": 50,
168
+ "50": 50,
169
+ "4rows": 63,
170
+ "4row": 63,
171
+ "4": 63,
172
+ "63": 63
173
+ };
174
+ function mapSnapMode(mode) {
175
+ if (!mode) return 6;
176
+ return SNAP_MODE_MAP[mode.toLowerCase()] ?? 6;
177
+ }
178
+ function mapRangeMode(mode) {
179
+ if (!mode) return 2;
180
+ return RANGE_MODE_MAP[mode.toLowerCase()] ?? 2;
181
+ }
182
+ function mapStartMode(mode) {
183
+ if (!mode) return 2;
184
+ return START_MODE_MAP[mode.toLowerCase().replace(/\s+/g, "")] ?? 2;
185
+ }
186
+ function mapUnitOfDistance(unit) {
187
+ if (!unit) return "Kilometers";
188
+ const v = unit.toLowerCase();
189
+ if (["km", "kilometer", "kilometers"].includes(v)) return "Kilometers";
190
+ if (["mi", "mile", "miles"].includes(v)) return "Miles";
191
+ return unit;
192
+ }
193
+ function componentToGlName(name) {
194
+ return COMPONENT_TO_GL[name] ?? name;
195
+ }
196
+ function componentToContext(name) {
197
+ return COMPONENT_TO_CONTEXT[name] ?? name.toLowerCase();
198
+ }
199
+ function mapViewPreset(value) {
200
+ if (value === void 0 || value === null) return "2";
201
+ const v = String(value).toLowerCase().replace(/[\s_-]+/g, "");
202
+ return VIEW_PRESET_MAP[v] ?? String(value);
203
+ }
204
+ function mapRowHeight(value) {
205
+ if (value === void 0 || value === null) return 38;
206
+ const v = String(value).toLowerCase().replace(/[\s_-]+/g, "");
207
+ if (ROW_HEIGHT_MAP[v] !== void 0) return ROW_HEIGHT_MAP[v];
208
+ const parsed = parseInt(String(value), 10);
209
+ return isNaN(parsed) ? 38 : parsed;
210
+ }
211
+ function workspaceNodeToGL(node) {
212
+ const gl = { type: node.type };
213
+ if ("width" in node && node.width !== void 0) {
214
+ gl.width = node.width;
215
+ }
216
+ if ("height" in node && node.height !== void 0) {
217
+ gl.height = node.height;
218
+ }
219
+ if (node.type === "row" || node.type === "column" || node.type === "stack") {
220
+ gl.isClosable = true;
221
+ gl.reorderEnabled = true;
222
+ gl.title = "";
223
+ if ("content" in node && node.content) {
224
+ gl.content = node.content.map(workspaceNodeToGL);
225
+ }
226
+ if (node.type === "stack") {
227
+ gl.activeItemIndex = 0;
228
+ }
229
+ return gl;
230
+ }
231
+ if (node.type === "component") {
232
+ const compName = componentToGlName(node.component);
233
+ if (compName) {
234
+ gl.componentName = compName;
235
+ }
236
+ if (node.title !== void 0) {
237
+ gl.title = node.title;
238
+ }
239
+ gl.isClosable = true;
240
+ gl.reorderEnabled = true;
241
+ const compState = {};
242
+ const compId = node.id;
243
+ if (compId) {
244
+ gl.id = compId;
245
+ compState.id = compId;
246
+ } else if (compName === "schedulerComponent") {
247
+ gl.id = "schedulerContainer";
248
+ compState.id = "scheduler";
249
+ }
250
+ if (compName === "datePickerComponent" && compId) {
251
+ compState.wrapperId = compId;
252
+ }
253
+ if (Object.keys(compState).length > 0) {
254
+ gl.componentState = compState;
255
+ }
256
+ return gl;
257
+ }
258
+ return gl;
259
+ }
260
+ function workspaceToGoldenLayout(workspace) {
261
+ return {
262
+ settings: {
263
+ hasHeaders: true,
264
+ constrainDragToContainer: false,
265
+ reorderEnabled: true,
266
+ selectionEnabled: true,
267
+ popoutWholeStack: false,
268
+ blockedPopoutsThrowError: true,
269
+ closePopoutsOnUnload: true,
270
+ showPopoutIcon: false,
271
+ showMaximiseIcon: true,
272
+ showCloseIcon: true,
273
+ responsiveMode: "onload",
274
+ tabOverlapAllowance: 0,
275
+ reorderOnTabMenuClick: true,
276
+ tabControlOffset: 10
277
+ },
278
+ dimensions: {
279
+ borderWidth: 5,
280
+ borderGrabWidth: 15,
281
+ minItemHeight: 10,
282
+ minItemWidth: 10,
283
+ headerHeight: 20,
284
+ dragProxyWidth: 300,
285
+ dragProxyHeight: 200
286
+ },
287
+ labels: {
288
+ close: "Close",
289
+ maximise: "Maximize",
290
+ minimise: "Minimize",
291
+ popout: "open in new window",
292
+ popin: "pop in",
293
+ tabDropdown: "Additional Tabs"
294
+ },
295
+ content: [
296
+ {
297
+ type: "row",
298
+ isClosable: true,
299
+ reorderEnabled: true,
300
+ title: "",
301
+ content: workspace.map(workspaceNodeToGL)
302
+ }
303
+ ],
304
+ isClosable: true,
305
+ reorderEnabled: true,
306
+ title: "",
307
+ openPopouts: [],
308
+ maximisedItemId: null
309
+ };
310
+ }
311
+ function buildLayoutState(layout, gridName) {
312
+ const columns = (layout.columns || []).filter((c) => c.property).map((c) => ({ dataIndex: c.property }));
313
+ const sorters = (layout.sorters || []).filter((s) => s.property && s.direction).map((s) => ({ direction: s.direction, property: s.property }));
314
+ let grouper = null;
315
+ if (layout.grouper && layout.grouper.property && layout.grouper.direction) {
316
+ grouper = {
317
+ root: "data",
318
+ property: layout.grouper.property,
319
+ direction: layout.grouper.direction,
320
+ id: layout.grouper.id || layout.grouper.property
321
+ };
322
+ }
323
+ const filters = (layout.filters || []).map((f) => ({
324
+ operator: f.operator || "like",
325
+ value: f.value || "",
326
+ property: f.property,
327
+ exactMatch: f.exactMatch ?? false
328
+ }));
329
+ const lockedGridWidth = layout.lockedGridWidth || layout.resourcesGridWidth || null;
330
+ const fitToScreen = typeof layout.fitToScreen === "boolean" ? layout.fitToScreen : layout.fitToScreen === "true";
331
+ const ignoreCalendars = typeof layout.ignoreCalendars === "boolean" ? layout.ignoreCalendars : layout.ignoreCalendars === "true";
332
+ const pageSize = layout.pageSize !== void 0 ? typeof layout.pageSize === "number" ? layout.pageSize : parseInt(String(layout.pageSize), 10) : 50;
333
+ const state = {
334
+ columns,
335
+ storeState: grouper ? { grouper } : {},
336
+ filters,
337
+ viewPreset: layout.viewPreset !== void 0 ? mapViewPreset(layout.viewPreset) : null,
338
+ Id: layout.id || gridName,
339
+ lockedGridWidth,
340
+ pageSize: isNaN(pageSize) ? 50 : pageSize,
341
+ fitToScreen,
342
+ rowHeight: mapRowHeight(layout.rowHeight),
343
+ ignoreCalendars
344
+ };
345
+ if (sorters.length > 0) {
346
+ state.sorters = sorters;
347
+ }
348
+ if (layout.default) {
349
+ state.default = true;
350
+ }
351
+ if (layout.name) {
352
+ state.name = layout.name;
353
+ }
354
+ const extraFields = [
355
+ "viewLayoutType",
356
+ "startRowGroupsCollapsed",
357
+ "startColGroupsCollapsed",
358
+ "enableColumnSort",
359
+ "expandedItems",
360
+ "matrix",
361
+ "pivotcolumns",
362
+ "collapsed",
363
+ "hideEmptyRows"
364
+ ];
365
+ for (const field of extraFields) {
366
+ if (layout[field] !== void 0) {
367
+ state[field] = layout[field];
368
+ }
369
+ }
370
+ return state;
371
+ }
372
+ function collectLayouts(nodes) {
373
+ const acc = [];
374
+ for (const node of nodes) {
375
+ if (node.type === "row" || node.type === "column" || node.type === "stack") {
376
+ if ("content" in node && node.content) {
377
+ acc.push(...collectLayouts(node.content));
378
+ }
379
+ } else if (node.type === "component") {
380
+ const layouts = node.layouts || [];
381
+ const layoutArray = Array.isArray(layouts) ? layouts : [layouts];
382
+ for (const layoutItem of layoutArray) {
383
+ const context = componentToContext(node.component);
384
+ let gridName;
385
+ if (node.component === "planningBoard" && !layoutItem.id) {
386
+ gridName = "scheduler_panel";
387
+ } else {
388
+ gridName = layoutItem.id || `${node.id}_panel`;
389
+ }
390
+ const layoutName = layoutItem.name || node.title || gridName;
391
+ let layoutCode = layoutItem.code;
392
+ if (!layoutCode && gridName) {
393
+ layoutCode = gridName.toUpperCase().replace(/-/g, "_");
394
+ }
395
+ acc.push({
396
+ context,
397
+ grid: gridName,
398
+ value: buildLayoutState(layoutItem, gridName),
399
+ name: layoutName,
400
+ code: layoutCode || gridName.toUpperCase(),
401
+ componentId: gridName,
402
+ default: layoutItem.default ?? false,
403
+ public: layoutItem.shared?.public ?? false,
404
+ userGroup: layoutItem.shared?.userGroup
405
+ });
406
+ }
407
+ }
408
+ }
409
+ return acc;
410
+ }
411
+ function buildLayoutDtos(workspace) {
412
+ const layouts = collectLayouts(workspace);
413
+ return layouts.map((L) => ({
414
+ context: L.context,
415
+ grid: L.grid,
416
+ value: JSON.stringify(L.value),
417
+ default: L.default,
418
+ public: L.public,
419
+ userGroup: L.userGroup,
420
+ isOwner: true,
421
+ isNew: true,
422
+ name: L.name,
423
+ code: L.code
424
+ }));
425
+ }
426
+ function buildProfileLayoutDtos(workspace, profileCode) {
427
+ const layouts = collectLayouts(workspace);
428
+ return layouts.filter((L) => L.default && L.code).map((L) => ({
429
+ profileCode,
430
+ layoutCode: L.code,
431
+ default: true,
432
+ public: false,
433
+ name: L.componentId
434
+ }));
435
+ }
436
+ function compileProfile(profile) {
437
+ const theme = profile.theme || { color: "default", scheme: "sdtd" };
438
+ const shared = profile.shared || { global: false, userGroup: "" };
439
+ const route = profile.route || {
440
+ profile: "default",
441
+ calculateRoutes: true,
442
+ showSequenceIndicators: true,
443
+ unitOfDistance: "km"
444
+ };
445
+ const planning = profile.planning || {
446
+ snapInterval: { mode: "1hour", value: 0 },
447
+ range: { mode: "week", value: 0 },
448
+ start: { mode: "startOfWeek", value: 2 },
449
+ hours: { start: 8, end: 18 }
450
+ };
451
+ const workspace = profile.workspace || [];
452
+ const goldenLayout = workspaceToGoldenLayout(workspace);
453
+ const layouts = buildLayoutDtos(workspace);
454
+ const profileLayouts = buildProfileLayoutDtos(workspace, profile.code || "");
455
+ return {
456
+ profile: {
457
+ name: profile.name || "",
458
+ code: profile.code || "",
459
+ color: theme.color || "default",
460
+ theme: theme.scheme || "sdtd",
461
+ public: shared.global ?? false,
462
+ userGroup: shared.userGroup || "",
463
+ snapMode: mapSnapMode(planning.snapInterval?.mode),
464
+ snapValue: planning.snapInterval?.value ?? null,
465
+ rangeMode: mapRangeMode(planning.range?.mode),
466
+ rangeValue: planning.range?.value ?? null,
467
+ startMode: mapStartMode(planning.start?.mode),
468
+ startValue: planning.start?.value ?? null,
469
+ startTime: planning.hours?.start ?? 8,
470
+ endTime: planning.hours?.end ?? 18,
471
+ unitOfDistance: mapUnitOfDistance(route.unitOfDistance),
472
+ autoRouteCalculation: route.calculateRoutes ?? true,
473
+ showSequenceIndicators: route.showSequenceIndicators ?? true,
474
+ routeProfile: route.profile || "default",
475
+ layout: JSON.stringify(goldenLayout)
476
+ },
477
+ layouts,
478
+ profileLayouts,
479
+ users: profile.users || [],
480
+ owner: profile.owner,
481
+ notificationEmail: profile.notificationEmail
482
+ };
483
+ }
484
+
485
+ // src/commands/validate.ts
486
+ var validateCommand = new Command("validate").description("Validate a configuration file").argument("<file>", "Path to the configuration file (.json5 or .json)").option("--json", "Output results as JSON").action((file, options) => {
487
+ try {
488
+ const filePath = resolve(normalize(file));
489
+ const content = readFileSync(filePath, "utf-8");
490
+ let profile;
491
+ try {
492
+ profile = JSON5.parse(content);
493
+ } catch {
494
+ if (options.json) {
495
+ console.log(JSON.stringify({ valid: false, errors: [{ type: "error", message: "Invalid JSON5 syntax" }], warnings: [] }));
496
+ } else {
497
+ console.error(pc.red("Error:"), "Invalid JSON5 syntax in", file);
498
+ }
499
+ process.exit(1);
500
+ }
501
+ const result = validateProfile(profile);
502
+ if (options.json) {
503
+ console.log(JSON.stringify(result, null, 2));
504
+ } else {
505
+ if (result.valid) {
506
+ console.log(pc.green("\u2713"), "Configuration is valid");
507
+ } else {
508
+ console.log(pc.red("\u2717"), "Configuration has errors");
509
+ }
510
+ if (result.errors.length > 0) {
511
+ console.log();
512
+ console.log(pc.red("Errors:"));
513
+ for (const error of result.errors) {
514
+ console.log(pc.red(" \u2022"), error.message);
515
+ }
516
+ }
517
+ if (result.warnings.length > 0) {
518
+ console.log();
519
+ console.log(pc.yellow("Warnings:"));
520
+ for (const warning of result.warnings) {
521
+ console.log(pc.yellow(" \u2022"), warning.message);
522
+ }
523
+ }
524
+ }
525
+ process.exit(result.valid ? 0 : 1);
526
+ } catch (err) {
527
+ if (options.json) {
528
+ console.log(JSON.stringify({ valid: false, errors: [{ type: "error", message: String(err) }], warnings: [] }));
529
+ } else {
530
+ console.error(pc.red("Error:"), String(err));
531
+ }
532
+ process.exit(1);
533
+ }
534
+ });
535
+
536
+ // src/commands/compile.ts
537
+ import { Command as Command2 } from "commander";
538
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
539
+ import { resolve as resolve2, normalize as normalize2, dirname, basename, extname } from "path";
540
+ import JSON52 from "json5";
541
+ import pc2 from "picocolors";
542
+ var compileCommand = new Command2("compile").description("Compile a JSON5 configuration to API-ready JSON").argument("<file>", "Path to the configuration file (.json5)").option("-o, --output <dir>", "Output directory (default: same as input)").option("--stdout", "Output to stdout instead of file").option("--skip-validation", "Skip validation before compiling").action((file, options) => {
543
+ try {
544
+ const filePath = resolve2(normalize2(file));
545
+ const content = readFileSync2(filePath, "utf-8");
546
+ let profile;
547
+ try {
548
+ profile = JSON52.parse(content);
549
+ } catch {
550
+ console.error(pc2.red("Error:"), "Invalid JSON5 syntax in", file);
551
+ process.exit(1);
552
+ }
553
+ if (!options.skipValidation) {
554
+ const validation = validateProfile(profile);
555
+ if (!validation.valid) {
556
+ console.error(pc2.red("Error:"), "Configuration has validation errors:");
557
+ for (const error of validation.errors) {
558
+ console.error(pc2.red(" \u2022"), error.message);
559
+ }
560
+ console.error();
561
+ console.error("Use --skip-validation to compile anyway");
562
+ process.exit(1);
563
+ }
564
+ }
565
+ const payload = compileProfile(profile);
566
+ const jsonOutput = JSON.stringify(payload, null, 2);
567
+ if (options.stdout) {
568
+ console.log(jsonOutput);
569
+ } else {
570
+ const inputBasename = basename(file, extname(file));
571
+ const outputDir = options.output ? resolve2(normalize2(options.output)) : dirname(filePath);
572
+ const outputPath = resolve2(outputDir, `${inputBasename}.json`);
573
+ mkdirSync(outputDir, { recursive: true });
574
+ writeFileSync(outputPath, jsonOutput, "utf-8");
575
+ console.log(pc2.green("\u2713"), "Compiled to", outputPath);
576
+ }
577
+ process.exit(0);
578
+ } catch (err) {
579
+ console.error(pc2.red("Error:"), String(err));
580
+ process.exit(1);
581
+ }
582
+ });
583
+
584
+ // src/commands/deploy.ts
585
+ import { Command as Command3 } from "commander";
586
+ import { readFileSync as readFileSync3 } from "fs";
587
+ import { resolve as resolve3, normalize as normalize3 } from "path";
588
+ import { createInterface } from "readline";
589
+ import JSON53 from "json5";
590
+ import pc3 from "picocolors";
591
+ var ENVIRONMENT_URLS = {
592
+ production: "https://api.dimescheduler.com",
593
+ sandbox: "https://sandbox.api.dimescheduler.com",
594
+ test: "https://test.api.dimescheduler.com"
595
+ };
596
+ async function confirm(message) {
597
+ const rl = createInterface({
598
+ input: process.stdin,
599
+ output: process.stdout
600
+ });
601
+ return new Promise((resolve4) => {
602
+ rl.question(`${message} (y/N) `, (answer) => {
603
+ rl.close();
604
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
605
+ });
606
+ });
607
+ }
608
+ async function deployToApi(payload, url, apiKey) {
609
+ try {
610
+ const response = await fetch(`${url}/import/setup`, {
611
+ method: "POST",
612
+ headers: {
613
+ "Content-Type": "application/json",
614
+ "X-API-KEY": apiKey
615
+ },
616
+ body: JSON.stringify(payload)
617
+ });
618
+ if (response.ok) {
619
+ return { success: true, message: "Profile deployed successfully" };
620
+ } else {
621
+ const text = await response.text();
622
+ return { success: false, message: `HTTP ${response.status}`, error: text };
623
+ }
624
+ } catch (err) {
625
+ return { success: false, message: "Request failed", error: String(err) };
626
+ }
627
+ }
628
+ var deployCommand = new Command3("deploy").description("Deploy a configuration to a Dime Scheduler instance").argument("<file>", "Path to the configuration file (.json5 or .json)").requiredOption("--api-key <key>", "API key for authentication (or set DS_API_KEY env var)").requiredOption("--env <environment>", "Environment: production, sandbox, or test").option("--skip-validation", "Skip validation before deploying").option("--json", "Output results as JSON").option("--dry-run", "Validate and compile but do not deploy").option("-y, --yes", "Skip confirmation prompt").action(async (file, options) => {
629
+ try {
630
+ const filePath = resolve3(normalize3(file));
631
+ const content = readFileSync3(filePath, "utf-8");
632
+ const apiKey = options.apiKey || process.env.DS_API_KEY;
633
+ if (!apiKey) {
634
+ if (options.json) {
635
+ console.log(JSON.stringify({ success: false, error: "API key is required. Use --api-key or set DS_API_KEY environment variable." }));
636
+ } else {
637
+ console.error(pc3.red("Error:"), "API key is required. Use --api-key or set DS_API_KEY environment variable.");
638
+ }
639
+ process.exit(1);
640
+ }
641
+ if (!ENVIRONMENT_URLS[options.env]) {
642
+ const validEnvs = Object.keys(ENVIRONMENT_URLS).join(", ");
643
+ if (options.json) {
644
+ console.log(JSON.stringify({ success: false, error: `Invalid environment: ${options.env}. Valid options: ${validEnvs}` }));
645
+ } else {
646
+ console.error(pc3.red("Error:"), `Invalid environment: ${options.env}. Valid options: ${validEnvs}`);
647
+ }
648
+ process.exit(1);
649
+ }
650
+ const apiUrl = ENVIRONMENT_URLS[options.env];
651
+ let profile;
652
+ try {
653
+ profile = JSON53.parse(content);
654
+ } catch {
655
+ if (options.json) {
656
+ console.log(JSON.stringify({ success: false, error: "Invalid JSON5 syntax" }));
657
+ } else {
658
+ console.error(pc3.red("Error:"), "Invalid JSON5 syntax in", file);
659
+ }
660
+ process.exit(1);
661
+ }
662
+ if (!options.skipValidation) {
663
+ const validation = validateProfile(profile);
664
+ if (!validation.valid) {
665
+ if (options.json) {
666
+ console.log(JSON.stringify({ success: false, error: "Validation failed", details: validation.errors }));
667
+ } else {
668
+ console.error(pc3.red("Error:"), "Configuration has validation errors:");
669
+ for (const error of validation.errors) {
670
+ console.error(pc3.red(" \u2022"), error.message);
671
+ }
672
+ }
673
+ process.exit(1);
674
+ }
675
+ }
676
+ const payload = compileProfile(profile);
677
+ if (options.dryRun) {
678
+ if (options.json) {
679
+ console.log(JSON.stringify({ success: true, message: "Dry run successful", payload }));
680
+ } else {
681
+ console.log(pc3.green("\u2713"), "Dry run successful - configuration is valid and compiled");
682
+ console.log(pc3.dim(" Would deploy to:"), apiUrl);
683
+ }
684
+ process.exit(0);
685
+ }
686
+ if (!options.yes && !options.json) {
687
+ console.log();
688
+ console.log(pc3.bold("Deploy Summary:"));
689
+ console.log(` Profile: ${pc3.cyan(profile.name || "unnamed")} (${profile.code || "no code"})`);
690
+ console.log(` Environment: ${pc3.yellow(options.env)}`);
691
+ console.log(` Target: ${pc3.dim(apiUrl)}`);
692
+ console.log();
693
+ const confirmed = await confirm("Proceed with deployment?");
694
+ if (!confirmed) {
695
+ console.log(pc3.dim("Deployment cancelled."));
696
+ process.exit(0);
697
+ }
698
+ }
699
+ if (!options.json) {
700
+ console.log(pc3.dim("Deploying..."));
701
+ }
702
+ const result = await deployToApi(payload, apiUrl, apiKey);
703
+ if (options.json) {
704
+ console.log(JSON.stringify(result));
705
+ } else {
706
+ if (result.success) {
707
+ console.log(pc3.green("\u2713"), result.message);
708
+ } else {
709
+ console.error(pc3.red("\u2717"), result.message);
710
+ if (result.error) {
711
+ console.error(pc3.dim(" "), result.error);
712
+ }
713
+ }
714
+ }
715
+ process.exit(result.success ? 0 : 2);
716
+ } catch (err) {
717
+ if (options.json) {
718
+ console.log(JSON.stringify({ success: false, error: String(err) }));
719
+ } else {
720
+ console.error(pc3.red("Error:"), String(err));
721
+ }
722
+ process.exit(1);
723
+ }
724
+ });
725
+
726
+ // src/index.ts
727
+ var program = new Command4();
728
+ program.name("dimescheduler-setup").description("CLI tool to validate, compile, and deploy Dime Scheduler configurations").version("0.1.0");
729
+ program.addCommand(validateCommand);
730
+ program.addCommand(compileCommand);
731
+ program.addCommand(deployCommand);
732
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@dimescheduler/setup",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to validate, compile, and deploy Dime Scheduler configurations",
5
+ "type": "module",
6
+ "bin": {
7
+ "dimescheduler-setup": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean",
16
+ "watch": "tsup src/index.ts --format esm --watch",
17
+ "start": "node dist/index.js",
18
+ "lint": "eslint src/",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^12.1.0",
23
+ "json5": "^2.2.3",
24
+ "picocolors": "^1.1.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.10.0",
28
+ "tsup": "^8.3.5",
29
+ "typescript": "^5.7.2"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/dimescheduler/dsl.git",
37
+ "directory": "packages/cli"
38
+ },
39
+ "keywords": [
40
+ "dimescheduler",
41
+ "cli",
42
+ "configuration",
43
+ "scheduling"
44
+ ],
45
+ "author": "Dime Software",
46
+ "license": "MIT"
47
+ }