@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 +404 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +732 -0
- package/package.json +47 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|