trackops 2.0.5 → 2.0.6
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 +91 -61
- package/bin/trackops.js +28 -7
- package/lib/cli-format.js +118 -0
- package/lib/config.js +29 -3
- package/lib/control.js +278 -116
- package/lib/env.js +40 -28
- package/lib/i18n.js +5 -4
- package/lib/init.js +149 -41
- package/lib/opera-bootstrap.js +251 -71
- package/lib/opera.js +235 -73
- package/lib/skills.js +43 -35
- package/lib/workspace.js +32 -21
- package/locales/en.json +183 -61
- package/locales/es.json +184 -62
- package/package.json +1 -1
- package/scripts/smoke-tests.js +81 -39
- package/skills/trackops/skill.json +2 -2
package/README.md
CHANGED
|
@@ -98,11 +98,13 @@ trackops --version
|
|
|
98
98
|
cd ruta/a/tu/proyecto
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
4. Activa TrackOps y elige el idioma del proyecto cuando el CLI lo pida:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
trackops init
|
|
105
|
-
```
|
|
101
|
+
4. Activa TrackOps y elige el idioma del proyecto cuando el CLI lo pida:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
trackops init
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`trackops init` funciona sobre directorios vacios, repos ya existentes y workspaces ya activados. Si lo ejecutas otra vez, refresca los artefactos gestionados. `trackops workspace migrate` queda reservado para proyectos TrackOps legacy.
|
|
106
108
|
|
|
107
109
|
5. Instala OPERA:
|
|
108
110
|
|
|
@@ -141,13 +143,15 @@ Y despues reanuda:
|
|
|
141
143
|
trackops opera bootstrap --resume
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
8. Si OPERA completa bootstrap directo, revisa estado y continua con:
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
trackops opera status
|
|
148
|
-
trackops next
|
|
149
|
-
trackops sync
|
|
150
|
-
```
|
|
146
|
+
8. Si OPERA completa bootstrap directo, revisa estado y continua con:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
trackops opera status
|
|
150
|
+
trackops next
|
|
151
|
+
trackops sync
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Si necesitas una salida mas simple para lectores de pantalla, logs o terminales con soporte Unicode limitado, añade `--plain` o `--a11y` a cualquier comando, por ejemplo `trackops status --plain`.
|
|
151
155
|
|
|
152
156
|
### Desinstalacion global y local
|
|
153
157
|
|
|
@@ -198,16 +202,18 @@ trackops init
|
|
|
198
202
|
trackops opera install
|
|
199
203
|
```
|
|
200
204
|
|
|
201
|
-
Semantica:
|
|
202
|
-
|
|
203
|
-
- `trackops init`
|
|
204
|
-
activa TrackOps en el repo
|
|
205
|
-
- `trackops opera install`
|
|
206
|
-
anade OPERA cuando quieres el framework operativo completo
|
|
207
|
-
- `trackops init --with-opera`
|
|
208
|
-
existe como atajo
|
|
209
|
-
- `trackops init --legacy-layout`
|
|
210
|
-
existe solo por compatibilidad
|
|
205
|
+
Semantica:
|
|
206
|
+
|
|
207
|
+
- `trackops init`
|
|
208
|
+
activa o actualiza TrackOps en el repo, adopta repos existentes y puede reejecutarse con seguridad
|
|
209
|
+
- `trackops opera install`
|
|
210
|
+
anade OPERA cuando quieres el framework operativo completo
|
|
211
|
+
- `trackops init --with-opera`
|
|
212
|
+
existe como atajo
|
|
213
|
+
- `trackops init --legacy-layout`
|
|
214
|
+
existe solo por compatibilidad
|
|
215
|
+
|
|
216
|
+
`trackops workspace migrate` se mantiene solo para proyectos TrackOps legacy.
|
|
211
217
|
|
|
212
218
|
### Workspace split
|
|
213
219
|
|
|
@@ -262,23 +268,32 @@ trackops opera bootstrap --resume
|
|
|
262
268
|
|
|
263
269
|
La terminal tambien debe decirte este siguiente paso al terminar el handoff.
|
|
264
270
|
|
|
265
|
-
#### Ya tengo un repo
|
|
266
|
-
|
|
267
|
-
Si el usuario es tecnico y el proyecto ya tiene suficiente contexto, OPERA sigue por bootstrap directo
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
#### Ya tengo un repo
|
|
272
|
+
|
|
273
|
+
Si el usuario es tecnico y el proyecto ya tiene suficiente contexto, OPERA sigue por bootstrap directo. En ese modo crea o actualiza desde el primer paso:
|
|
274
|
+
|
|
275
|
+
- `ops/bootstrap/intake.json`
|
|
276
|
+
- `ops/bootstrap/spec-dossier.md`
|
|
277
|
+
- `ops/bootstrap/open-questions.md`
|
|
278
|
+
- `ops/bootstrap/quality-report.json`
|
|
279
|
+
|
|
280
|
+
Despues solo compila `ops/contract/operating-contract.json` y recompila `ops/genesis.md` cuando el bootstrap ya es consistente.
|
|
281
|
+
|
|
282
|
+
Tambien puedes forzar el modo:
|
|
270
283
|
|
|
271
284
|
```bash
|
|
272
285
|
trackops opera install --bootstrap-mode handoff
|
|
273
286
|
trackops opera install --bootstrap-mode direct
|
|
274
287
|
```
|
|
275
288
|
|
|
276
|
-
Flags disponibles:
|
|
277
|
-
|
|
278
|
-
- `--technical-level low|medium|high|senior`
|
|
279
|
-
- `--project-state idea|draft|existing_repo|advanced`
|
|
280
|
-
- `--docs-state none|notes|sos|spec_dossier|repo_docs`
|
|
281
|
-
- `--decision-ownership user|shared|agent`
|
|
289
|
+
Flags disponibles:
|
|
290
|
+
|
|
291
|
+
- `--technical-level low|medium|high|senior`
|
|
292
|
+
- `--project-state idea|draft|existing_repo|advanced`
|
|
293
|
+
- `--docs-state none|notes|sos|spec_dossier|repo_docs`
|
|
294
|
+
- `--decision-ownership user|shared|agent`
|
|
295
|
+
|
|
296
|
+
En modo directo, `trackops opera handoff` no inventa un handoff de agente: muestra un resumen guiado de esos archivos y te dice exactamente que completar antes de reanudar.
|
|
282
297
|
|
|
283
298
|
### Entorno y secretos
|
|
284
299
|
|
|
@@ -319,12 +334,12 @@ trackops doctor locale
|
|
|
319
334
|
|
|
320
335
|
| Comando | Descripcion |
|
|
321
336
|
|---|---|
|
|
322
|
-
| `trackops init [--with-opera] [--locale es\|en] [--name "..."] [--no-bootstrap] [--legacy-layout]` | Inicializa TrackOps |
|
|
337
|
+
| `trackops init [--with-opera] [--locale es\|en] [--name "..."] [--no-bootstrap] [--legacy-layout]` | Inicializa o actualiza TrackOps; adopta repos existentes |
|
|
323
338
|
| `trackops status` | Muestra estado operativo |
|
|
324
339
|
| `trackops next` | Muestra la siguiente cola priorizada |
|
|
325
340
|
| `trackops sync` | Regenera docs operativos |
|
|
326
341
|
| `trackops workspace status` | Muestra layout y roots |
|
|
327
|
-
| `trackops workspace migrate` | Migra un proyecto legacy |
|
|
342
|
+
| `trackops workspace migrate` | Migra un proyecto TrackOps legacy |
|
|
328
343
|
| `trackops env status` | Audita claves presentes y faltantes |
|
|
329
344
|
| `trackops env sync` | Regenera `/.env`, `/.env.example` y el puente |
|
|
330
345
|
| `trackops locale get\|set [es\|en]` | Lee o fija el idioma global |
|
|
@@ -334,9 +349,11 @@ trackops doctor locale
|
|
|
334
349
|
| `trackops opera install [--bootstrap-mode ...] [--technical-level ...] [--project-state ...] [--docs-state ...] [--decision-ownership ...]` | Instala OPERA y decide la ruta de bootstrap |
|
|
335
350
|
| `trackops opera bootstrap [--resume]` | Continua el bootstrap o ingiere el resultado del agente |
|
|
336
351
|
| `trackops opera handoff [--print\|--json]` | Muestra el handoff listo para copiar al agente |
|
|
337
|
-
| `trackops opera status` | Muestra estado de instalacion y bootstrap |
|
|
338
|
-
| `trackops opera configure` | Reconfigura idioma o fases |
|
|
339
|
-
| `trackops opera upgrade --stable [--reset]` | Reescribe artefactos gestionados a la version estable actual |
|
|
352
|
+
| `trackops opera status` | Muestra estado de instalacion y bootstrap |
|
|
353
|
+
| `trackops opera configure` | Reconfigura idioma o fases |
|
|
354
|
+
| `trackops opera upgrade --stable [--reset]` | Reescribe artefactos gestionados a la version estable actual |
|
|
355
|
+
|
|
356
|
+
Todos los comandos aceptan `--plain` y `--a11y` para una salida mas lineal y accesible.
|
|
340
357
|
|
|
341
358
|
### Skills del proyecto
|
|
342
359
|
|
|
@@ -463,11 +480,13 @@ trackops --version
|
|
|
463
480
|
cd path/to/your/project
|
|
464
481
|
```
|
|
465
482
|
|
|
466
|
-
4. Activate TrackOps and choose the project language when the CLI asks:
|
|
467
|
-
|
|
468
|
-
```bash
|
|
469
|
-
trackops init
|
|
470
|
-
```
|
|
483
|
+
4. Activate TrackOps and choose the project language when the CLI asks:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
trackops init
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`trackops init` works on empty directories, existing repositories, and already initialized workspaces. Rerunning it refreshes managed artifacts. `trackops workspace migrate` is reserved for legacy TrackOps projects.
|
|
471
490
|
|
|
472
491
|
5. Install OPERA:
|
|
473
492
|
|
|
@@ -504,13 +523,15 @@ Then resume with:
|
|
|
504
523
|
trackops opera bootstrap --resume
|
|
505
524
|
```
|
|
506
525
|
|
|
507
|
-
8. If OPERA completes direct bootstrap, review status and continue with:
|
|
508
|
-
|
|
509
|
-
```bash
|
|
510
|
-
trackops opera status
|
|
511
|
-
trackops next
|
|
512
|
-
trackops sync
|
|
513
|
-
```
|
|
526
|
+
8. If OPERA completes direct bootstrap, review status and continue with:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
trackops opera status
|
|
530
|
+
trackops next
|
|
531
|
+
trackops sync
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
If you need simpler output for screen readers, logs, or terminals with limited Unicode support, add `--plain` or `--a11y` to any command, for example `trackops status --plain`.
|
|
514
535
|
|
|
515
536
|
### Local activation
|
|
516
537
|
|
|
@@ -571,16 +592,25 @@ If the user is not technical, the project is still in idea stage, or documentati
|
|
|
571
592
|
trackops opera bootstrap --resume
|
|
572
593
|
```
|
|
573
594
|
|
|
574
|
-
#### I already have a repository
|
|
575
|
-
|
|
576
|
-
If the user is technical and the project already has enough context, OPERA continues with direct bootstrap
|
|
577
|
-
|
|
578
|
-
|
|
595
|
+
#### I already have a repository
|
|
596
|
+
|
|
597
|
+
If the user is technical and the project already has enough context, OPERA continues with direct bootstrap. In that mode it creates or updates these files from the start:
|
|
598
|
+
|
|
599
|
+
- `ops/bootstrap/intake.json`
|
|
600
|
+
- `ops/bootstrap/spec-dossier.md`
|
|
601
|
+
- `ops/bootstrap/open-questions.md`
|
|
602
|
+
- `ops/bootstrap/quality-report.json`
|
|
603
|
+
|
|
604
|
+
It only compiles `ops/contract/operating-contract.json` and recompiles `ops/genesis.md` once bootstrap becomes consistent.
|
|
605
|
+
|
|
606
|
+
You can also force the mode:
|
|
579
607
|
|
|
580
608
|
```bash
|
|
581
|
-
trackops opera install --bootstrap-mode handoff
|
|
582
|
-
trackops opera install --bootstrap-mode direct
|
|
583
|
-
```
|
|
609
|
+
trackops opera install --bootstrap-mode handoff
|
|
610
|
+
trackops opera install --bootstrap-mode direct
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
In direct mode, `trackops opera handoff` does not fake an agent handoff. It shows a guided summary of those files and the exact next step before resume.
|
|
584
614
|
|
|
585
615
|
### Global and local removal
|
|
586
616
|
|
|
@@ -684,9 +714,9 @@ trackops skill install <name>
|
|
|
684
714
|
trackops skill remove <name>
|
|
685
715
|
```
|
|
686
716
|
|
|
687
|
-
### Main CLI
|
|
688
|
-
|
|
689
|
-
Core and OPERA commands follow the same contract as the Spanish section above, including `trackops opera handoff`, `trackops opera bootstrap --resume`,
|
|
717
|
+
### Main CLI
|
|
718
|
+
|
|
719
|
+
Core and OPERA commands follow the same contract as the Spanish section above, including `trackops opera handoff`, `trackops opera bootstrap --resume`, the explicit `npm install -g trackops` runtime step, and the global `--plain` / `--a11y` flags.
|
|
690
720
|
|
|
691
721
|
### Publishing
|
|
692
722
|
|
package/bin/trackops.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const nodeVersion = parseInt(process.versions.node, 10);
|
|
4
|
+
if (nodeVersion < 18) {
|
|
5
|
+
console.error(`TrackOps requiere Node.js 18 o superior. Version actual: ${process.versions.node}`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const config = require("../lib/config");
|
|
11
|
+
const runtimeState = require("../lib/runtime-state");
|
|
12
|
+
const { setLocale, t } = require("../lib/i18n");
|
|
13
|
+
const fmt = require("../lib/cli-format");
|
|
14
|
+
const pkg = require("../package.json");
|
|
15
|
+
|
|
16
|
+
function parseCliTokens(argv) {
|
|
17
|
+
const plain = argv.includes("--plain") || argv.includes("--a11y");
|
|
18
|
+
const tokens = argv.filter((token) => !["--plain", "--a11y"].includes(token));
|
|
19
|
+
return {
|
|
20
|
+
plain,
|
|
21
|
+
command: tokens[0],
|
|
22
|
+
args: tokens.slice(1),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = parseCliTokens(process.argv.slice(2));
|
|
27
|
+
fmt.configure({ plain: parsed.plain });
|
|
28
|
+
|
|
29
|
+
const command = parsed.command;
|
|
30
|
+
const args = parsed.args;
|
|
11
31
|
|
|
12
32
|
function initCliLocale() {
|
|
13
33
|
let projectLocale = null;
|
|
@@ -27,6 +47,7 @@ function resolveRoot() {
|
|
|
27
47
|
const context = config.resolveWorkspaceContext();
|
|
28
48
|
if (!context) {
|
|
29
49
|
console.error(t("cli.error.noWorkspace"));
|
|
50
|
+
console.error(t("cli.help.initFirst"));
|
|
30
51
|
process.exit(1);
|
|
31
52
|
}
|
|
32
53
|
return context.workspaceRoot;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI output formatting helpers for accessible, scannable terminal output.
|
|
5
|
+
* Supports a plain mode for screen readers, logs, and limited terminals.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const state = {
|
|
9
|
+
plain: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const UNICODE_SEP = "\u2500".repeat(48);
|
|
13
|
+
const PLAIN_SEP = "-".repeat(48);
|
|
14
|
+
|
|
15
|
+
const STATUS_TOKENS = {
|
|
16
|
+
pending: ["\u23F3", "PENDING"],
|
|
17
|
+
in_progress: ["\uD83D\uDEA7", "IN PROGRESS"],
|
|
18
|
+
in_review: ["\uD83D\uDC40", "IN REVIEW"],
|
|
19
|
+
blocked: ["\u26D4", "BLOCKED"],
|
|
20
|
+
completed: ["\u2705", "DONE"],
|
|
21
|
+
cancelled: ["\uD83D\uDDD1\uFE0F", "CANCELLED"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const CHECK_TOKENS = {
|
|
25
|
+
pass: ["\u2705", "PASS"],
|
|
26
|
+
warn: ["\u26A0\uFE0F", "WARN"],
|
|
27
|
+
fail: ["\u274C", "FAIL"],
|
|
28
|
+
pending: ["\u23F3", "PENDING"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function configure(options = {}) {
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(options, "plain")) {
|
|
33
|
+
state.plain = Boolean(options.plain);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPlain() {
|
|
38
|
+
return state.plain;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pick(unicodeValue, plainValue) {
|
|
42
|
+
return state.plain ? plainValue : unicodeValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sep() {
|
|
46
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function blank() {
|
|
50
|
+
console.log("");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function header(text) {
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
56
|
+
console.log(` ${text}`);
|
|
57
|
+
console.log(pick(UNICODE_SEP, PLAIN_SEP));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function success(text) {
|
|
61
|
+
console.log(` ${pick("\u2713", "OK")} ${text}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function info(text) {
|
|
65
|
+
console.log(` ${text}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function step(label, value) {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
console.log(` ${pick("\u2192", "->")} ${label}: ${value}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` ${pick("\u2192", "->")} ${label}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hint(text) {
|
|
77
|
+
console.log(` ${text}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function warn(text) {
|
|
81
|
+
console.log(` ${pick("\u26A0", "WARN")} ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function bullet(text) {
|
|
85
|
+
console.log(` - ${text}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function statusToken(status) {
|
|
89
|
+
const pair = STATUS_TOKENS[status] || [status, String(status || "").toUpperCase()];
|
|
90
|
+
return pick(pair[0], `[${pair[1]}]`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkToken(status) {
|
|
94
|
+
const pair = CHECK_TOKENS[status] || [status, String(status || "").toUpperCase()];
|
|
95
|
+
return pick(pair[0], `[${pair[1]}]`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function boolToken(value) {
|
|
99
|
+
return value ? pick("\u2705", "[YES]") : pick("\u274C", "[NO]");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
configure,
|
|
104
|
+
isPlain,
|
|
105
|
+
pick,
|
|
106
|
+
sep,
|
|
107
|
+
blank,
|
|
108
|
+
header,
|
|
109
|
+
success,
|
|
110
|
+
info,
|
|
111
|
+
step,
|
|
112
|
+
hint,
|
|
113
|
+
warn,
|
|
114
|
+
bullet,
|
|
115
|
+
statusToken,
|
|
116
|
+
checkToken,
|
|
117
|
+
boolToken,
|
|
118
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -269,14 +269,40 @@ function getOperaVersion(control) {
|
|
|
269
269
|
return control.meta?.opera?.version || null;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function validateControl(control) {
|
|
273
|
+
if (!control || typeof control !== "object") throw new Error("project_control.json is not a valid object.");
|
|
274
|
+
if (!control.meta || typeof control.meta !== "object") throw new Error("project_control.json is missing required field: meta");
|
|
275
|
+
if (!Array.isArray(control.tasks)) throw new Error("project_control.json is missing required field: tasks");
|
|
276
|
+
for (let i = 0; i < control.tasks.length; i++) {
|
|
277
|
+
const task = control.tasks[i];
|
|
278
|
+
if (!task.id) throw new Error(`Task at index ${i} is missing required field: id`);
|
|
279
|
+
if (!task.status) throw new Error(`Task at index ${i} is missing required field: status`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
272
283
|
function loadControl(contextOrRoot) {
|
|
273
284
|
const filePath = controlFilePath(contextOrRoot);
|
|
274
|
-
|
|
285
|
+
let raw;
|
|
286
|
+
try {
|
|
287
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
throw new Error(`No se puede leer project_control.json.\n Ruta: ${filePath}\n Detalle: ${err.code === "ENOENT" ? "El archivo no existe. Ejecuta 'trackops init' primero." : err.message}`);
|
|
290
|
+
}
|
|
291
|
+
let control;
|
|
292
|
+
try {
|
|
293
|
+
control = JSON.parse(raw);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new Error(`project_control.json esta corrupto o no es JSON valido.\n Ruta: ${filePath}\n Detalle: ${err.message}`);
|
|
296
|
+
}
|
|
297
|
+
validateControl(control);
|
|
298
|
+
return control;
|
|
275
299
|
}
|
|
276
300
|
|
|
277
|
-
function saveControl(contextOrRoot, control) {
|
|
301
|
+
function saveControl(contextOrRoot, control, options) {
|
|
278
302
|
control.meta = control.meta || {};
|
|
279
|
-
|
|
303
|
+
if (!(options && options.skipTimestamp)) {
|
|
304
|
+
control.meta.updatedAt = new Date().toISOString();
|
|
305
|
+
}
|
|
280
306
|
const filePath = controlFilePath(contextOrRoot);
|
|
281
307
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
282
308
|
fs.writeFileSync(filePath, JSON.stringify(control, null, 2) + "\n", "utf8");
|