trackops 2.0.4 → 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/LICENSE +21 -21
- package/README.md +660 -575
- package/bin/trackops.js +127 -106
- package/lib/cli-format.js +118 -0
- package/lib/config.js +352 -326
- package/lib/control.js +408 -246
- package/lib/env.js +234 -222
- package/lib/i18n.js +5 -4
- package/lib/init.js +390 -282
- package/lib/locale.js +41 -41
- package/lib/opera-bootstrap.js +1066 -880
- package/lib/opera.js +615 -444
- package/lib/preferences.js +74 -74
- package/lib/registry.js +214 -214
- package/lib/release.js +56 -56
- package/lib/runtime-state.js +144 -144
- package/lib/skills.js +114 -89
- package/lib/workspace.js +259 -248
- package/locales/en.json +311 -167
- package/locales/es.json +314 -170
- package/package.json +61 -58
- package/scripts/postinstall-locale.js +21 -21
- package/scripts/skills-marketplace-smoke.js +124 -124
- package/scripts/smoke-tests.js +563 -517
- package/scripts/sync-skill-version.js +21 -21
- package/scripts/validate-skill.js +103 -103
- package/skills/trackops/SKILL.md +126 -122
- package/skills/trackops/agents/openai.yaml +7 -7
- package/skills/trackops/locales/en/SKILL.md +126 -122
- package/skills/trackops/locales/en/references/activation.md +94 -90
- package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
- package/skills/trackops/locales/en/references/workflow.md +55 -32
- package/skills/trackops/references/activation.md +94 -90
- package/skills/trackops/references/troubleshooting.md +73 -67
- package/skills/trackops/references/workflow.md +55 -32
- package/skills/trackops/skill.json +29 -29
- package/templates/hooks/post-checkout +2 -2
- package/templates/hooks/post-commit +2 -2
- package/templates/hooks/post-merge +2 -2
- package/templates/opera/agent.md +28 -27
- package/templates/opera/architecture/dependency-graph.md +24 -24
- package/templates/opera/architecture/runtime-automation.md +24 -24
- package/templates/opera/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/agent.md +22 -21
- package/templates/opera/en/architecture/dependency-graph.md +24 -24
- package/templates/opera/en/architecture/runtime-automation.md +24 -24
- package/templates/opera/en/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/reviews/delivery-audit.md +18 -18
- package/templates/opera/en/reviews/integration-audit.md +18 -18
- package/templates/opera/en/router.md +24 -19
- package/templates/opera/references/autonomy-and-recovery.md +117 -117
- package/templates/opera/references/opera-cycle.md +193 -193
- package/templates/opera/registry.md +28 -28
- package/templates/opera/reviews/delivery-audit.md +18 -18
- package/templates/opera/reviews/integration-audit.md +18 -18
- package/templates/opera/router.md +54 -49
- package/templates/skills/changelog-updater/SKILL.md +69 -69
- package/templates/skills/commiter/SKILL.md +99 -99
- package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
- package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
- package/templates/skills/opera-policy-guard/SKILL.md +26 -26
- package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
- package/templates/skills/opera-skill/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
- package/templates/skills/opera-skill/references/phase-dod.md +138 -0
- package/templates/skills/project-starter-skill/SKILL.md +150 -131
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
- package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
- package/ui/css/base.css +284 -284
- package/ui/css/charts.css +425 -425
- package/ui/css/components.css +1107 -1107
- package/ui/css/onboarding.css +133 -133
- package/ui/css/terminal.css +125 -125
- package/ui/css/timeline.css +58 -58
- package/ui/css/tokens.css +284 -284
- package/ui/favicon.svg +5 -5
- package/ui/index.html +99 -99
- package/ui/js/charts.js +526 -526
- package/ui/js/console-logger.js +172 -172
- package/ui/js/filters.js +247 -247
- package/ui/js/icons.js +129 -129
- package/ui/js/keyboard.js +229 -229
- package/ui/js/router.js +142 -142
- package/ui/js/theme.js +100 -100
- package/ui/js/time-tracker.js +248 -248
- package/ui/js/views/dashboard.js +870 -870
- package/ui/js/views/flash.js +47 -47
- package/ui/js/views/projects.js +745 -745
- package/ui/js/views/scrum.js +476 -476
- package/ui/js/views/settings.js +331 -331
- package/ui/js/views/timeline.js +265 -265
package/scripts/smoke-tests.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const assert = require("assert");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const http = require("http");
|
|
6
|
-
const net = require("net");
|
|
7
|
-
const os = require("os");
|
|
8
|
-
const path = require("path");
|
|
9
|
-
const { spawn, spawnSync } = require("child_process");
|
|
10
|
-
|
|
11
|
-
const ROOT = path.resolve(__dirname, "..");
|
|
12
|
-
const BIN = path.join(ROOT, "bin", "trackops.js");
|
|
13
|
-
const SKILL_VALIDATE = path.join(ROOT, "scripts", "validate-skill.js");
|
|
14
|
-
|
|
15
|
-
function getNpmCommand() {
|
|
16
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function runNode(args, cwd, envOverrides = {}) {
|
|
20
|
-
const result = spawnSync(process.execPath, args, {
|
|
21
|
-
cwd,
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
env: { ...process.env, ...envOverrides },
|
|
24
|
-
});
|
|
25
|
-
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
|
|
26
|
-
return result.stdout;
|
|
27
|
-
}
|
|
28
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const net = require("net");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawn, spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
12
|
+
const BIN = path.join(ROOT, "bin", "trackops.js");
|
|
13
|
+
const SKILL_VALIDATE = path.join(ROOT, "scripts", "validate-skill.js");
|
|
14
|
+
|
|
15
|
+
function getNpmCommand() {
|
|
16
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runNode(args, cwd, envOverrides = {}) {
|
|
20
|
+
const result = spawnSync(process.execPath, args, {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
env: { ...process.env, ...envOverrides },
|
|
24
|
+
});
|
|
25
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
|
|
26
|
+
return result.stdout;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
29
|
function runNodeResult(args, cwd, envOverrides = {}) {
|
|
30
30
|
return spawnSync(process.execPath, args, {
|
|
31
31
|
cwd,
|
|
@@ -34,521 +34,567 @@ function runNodeResult(args, cwd, envOverrides = {}) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
cwd,
|
|
40
|
-
encoding: "utf8",
|
|
41
|
-
env: { ...process.env, ...envOverrides },
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function runNpm(args, cwd, envOverrides = {}) {
|
|
46
|
-
return spawnSync(getNpmCommand(), args, {
|
|
37
|
+
function runNodeWithInput(args, cwd, input, envOverrides = {}) {
|
|
38
|
+
const result = spawnSync(process.execPath, args, {
|
|
47
39
|
cwd,
|
|
48
40
|
encoding: "utf8",
|
|
41
|
+
input,
|
|
49
42
|
env: { ...process.env, ...envOverrides },
|
|
50
|
-
shell: process.platform === "win32",
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function git(args, cwd) {
|
|
55
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
56
|
-
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `git ${args.join(" ")} fallo`);
|
|
57
|
-
return result.stdout.trim();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function wait(ms) {
|
|
61
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function get(port, pathname, host = "127.0.0.1") {
|
|
65
|
-
return new Promise((resolve, reject) => {
|
|
66
|
-
const req = http.get({ host, port, path: pathname }, (res) => {
|
|
67
|
-
let body = "";
|
|
68
|
-
res.setEncoding("utf8");
|
|
69
|
-
res.on("data", (chunk) => { body += chunk; });
|
|
70
|
-
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
71
|
-
});
|
|
72
|
-
req.on("error", reject);
|
|
73
43
|
});
|
|
44
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `fallo ejecutando ${args.join(" ")}`);
|
|
45
|
+
return result.stdout;
|
|
74
46
|
}
|
|
75
|
-
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
47
|
+
|
|
48
|
+
function runCommand(command, args, cwd, envOverrides = {}) {
|
|
49
|
+
return spawnSync(command, args, {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
env: { ...process.env, ...envOverrides },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runNpm(args, cwd, envOverrides = {}) {
|
|
57
|
+
return spawnSync(getNpmCommand(), args, {
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
env: { ...process.env, ...envOverrides },
|
|
61
|
+
shell: process.platform === "win32",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function git(args, cwd) {
|
|
66
|
+
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
67
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout || `git ${args.join(" ")} fallo`);
|
|
68
|
+
return result.stdout.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function wait(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function get(port, pathname, host = "127.0.0.1") {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const req = http.get({ host, port, path: pathname }, (res) => {
|
|
78
|
+
let body = "";
|
|
79
|
+
res.setEncoding("utf8");
|
|
80
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
81
|
+
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
82
|
+
});
|
|
83
|
+
req.on("error", reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractLocalPort(output) {
|
|
88
|
+
const match = String(output || "").match(/- Local:\s+http:\/\/[^\s:]+:(\d+)/);
|
|
89
|
+
return match ? Number(match[1]) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasExternalIpv4() {
|
|
93
|
+
return Object.values(os.networkInterfaces()).some((entries) => (
|
|
94
|
+
(entries || []).some((entry) => entry && entry.family === "IPv4" && !entry.internal)
|
|
95
|
+
));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPortFree(port, host = "127.0.0.1") {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const server = net.createServer();
|
|
101
|
+
server.once("error", () => resolve(false));
|
|
102
|
+
server.once("listening", () => {
|
|
103
|
+
server.close(() => resolve(true));
|
|
104
|
+
});
|
|
105
|
+
server.listen(port, host);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findFreePort(host = "127.0.0.1") {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const server = net.createServer();
|
|
112
|
+
server.once("error", reject);
|
|
113
|
+
server.listen(0, host, () => {
|
|
114
|
+
const address = server.address();
|
|
115
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
116
|
+
server.close((closeError) => {
|
|
117
|
+
if (closeError) reject(closeError);
|
|
118
|
+
else resolve(port);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function occupyPort(port, host = "127.0.0.1") {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const server = http.createServer((_req, res) => {
|
|
127
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
128
|
+
res.end("busy");
|
|
129
|
+
});
|
|
130
|
+
server.once("error", reject);
|
|
131
|
+
server.listen(port, host, () => resolve(server));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function startDashboard(cwd, args = [], envOverrides = {}) {
|
|
136
|
+
const env = { ...process.env, ...envOverrides };
|
|
137
|
+
const child = spawn(process.execPath, [BIN, "dashboard", ...args], {
|
|
138
|
+
cwd,
|
|
139
|
+
env,
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let output = "";
|
|
144
|
+
child.stdout.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
145
|
+
child.stderr.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
child,
|
|
149
|
+
output: () => output,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function waitForDashboard(session) {
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
|
|
156
|
+
while (Date.now() - startedAt < 15000) {
|
|
157
|
+
if (session.child.exitCode != null) {
|
|
158
|
+
throw new Error(`el dashboard termino antes de iniciar:\n${session.output()}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const port = extractLocalPort(session.output());
|
|
162
|
+
if (port) {
|
|
163
|
+
try {
|
|
164
|
+
const response = await get(port, "/api/state");
|
|
165
|
+
if (response.status === 200) {
|
|
166
|
+
return { port, output: session.output() };
|
|
167
|
+
}
|
|
168
|
+
} catch (_error) {
|
|
169
|
+
// sigue esperando
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await wait(250);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(`el dashboard no respondio a tiempo:\n${session.output()}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function stopDashboard(session) {
|
|
180
|
+
if (!session || !session.child || session.child.exitCode != null) return;
|
|
181
|
+
session.child.kill("SIGTERM");
|
|
182
|
+
await wait(500);
|
|
183
|
+
if (session.child.exitCode == null) {
|
|
184
|
+
session.child.kill("SIGKILL");
|
|
185
|
+
await wait(250);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writeJson(filePath, data) {
|
|
190
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
191
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function packCurrentPackage(tempRoot) {
|
|
195
|
+
const result = runNpm(["pack", ROOT], tempRoot);
|
|
196
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout || "npm pack fallo");
|
|
197
|
+
const tarballName = String(result.stdout || "").trim().split(/\r?\n/).pop();
|
|
198
|
+
const tarballPath = path.join(tempRoot, tarballName);
|
|
199
|
+
assert.ok(fs.existsSync(tarballPath), `no se encontro el tarball ${tarballPath}`);
|
|
200
|
+
return tarballPath;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readJson(filePath) {
|
|
204
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initGitRepo(repo) {
|
|
208
|
+
git(["init"], repo);
|
|
209
|
+
git(["config", "user.email", "smoke@example.com"], repo);
|
|
210
|
+
git(["config", "user.name", "Smoke Runner"], repo);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function commitAll(repo, message) {
|
|
214
|
+
git(["add", "."], repo);
|
|
215
|
+
git(["commit", "-m", message], repo);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
runNode([SKILL_VALIDATE], ROOT);
|
|
220
|
+
|
|
221
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "trackops-smoke-"));
|
|
222
|
+
const tarballPath = packCurrentPackage(tempRoot);
|
|
223
|
+
const packageVersion = readJson(path.join(ROOT, "package.json")).version;
|
|
224
|
+
|
|
225
|
+
const bootstrapHome = path.join(tempRoot, "bootstrap-home");
|
|
226
|
+
const bootstrapPrefix = path.join(tempRoot, "bootstrap-prefix");
|
|
227
|
+
fs.mkdirSync(bootstrapHome, { recursive: true });
|
|
228
|
+
fs.mkdirSync(bootstrapPrefix, { recursive: true });
|
|
229
|
+
|
|
230
|
+
const bootstrapEnv = {
|
|
231
|
+
TRACKOPS_BOOTSTRAP_HOME: bootstrapHome,
|
|
232
|
+
TRACKOPS_BOOTSTRAP_PREFIX: bootstrapPrefix,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const explicitInstall = runNpm(["install", "-g", "--prefix", bootstrapPrefix, tarballPath], tempRoot, bootstrapEnv);
|
|
236
|
+
assert.strictEqual(explicitInstall.status, 0, explicitInstall.stderr || explicitInstall.stdout || "instalacion global explicita fallo");
|
|
237
|
+
|
|
238
|
+
const runtimeStamp = readJson(path.join(bootstrapHome, ".trackops", "runtime.json"));
|
|
239
|
+
assert.ok(["es", "en"].includes(runtimeStamp.locale), "el bootstrap global debe fijar un idioma");
|
|
240
|
+
|
|
241
|
+
const installedCli = path.join(bootstrapPrefix, "node_modules", "trackops", "bin", "trackops.js");
|
|
242
|
+
assert.ok(fs.existsSync(installedCli), "el runtime instalado debe existir dentro del prefijo aislado");
|
|
243
|
+
const installedVersion = runNode([installedCli, "--version"], tempRoot);
|
|
244
|
+
assert.strictEqual(installedVersion.trim(), packageVersion);
|
|
245
|
+
|
|
235
246
|
const helpOutput = runNode([BIN, "help"], ROOT);
|
|
236
247
|
assert.doesNotMatch(helpOutput, /\btrackops agent\b/i);
|
|
237
248
|
assert.match(helpOutput, /workspace status\|migrate/i);
|
|
238
249
|
assert.match(helpOutput, /env status\|sync/i);
|
|
239
250
|
assert.match(helpOutput, /release \[--push\]/i);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
assert.ok(fs.existsSync(path.join(splitProject, ".
|
|
256
|
-
assert.ok(fs.existsSync(path.join(splitProject, ".env
|
|
257
|
-
assert.ok(fs.existsSync(path.join(splitProject, "
|
|
258
|
-
assert.ok(fs.existsSync(path.join(splitProject, "
|
|
259
|
-
assert.ok(fs.existsSync(path.join(splitProject, "
|
|
260
|
-
assert.ok(fs.existsSync(path.join(splitProject, "app", ".env
|
|
261
|
-
assert.ok(
|
|
262
|
-
assert.ok(!fs.existsSync(path.join(splitProject, "
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
assert.strictEqual(splitControl.meta.
|
|
267
|
-
assert.strictEqual(splitControl.meta.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
assert.match(projectLocaleDoctor, /
|
|
272
|
-
|
|
251
|
+
assert.match(helpOutput, /--plain|--a11y/i);
|
|
252
|
+
|
|
253
|
+
const versionOutput = runNode([BIN, "--version"], ROOT);
|
|
254
|
+
assert.strictEqual(versionOutput.trim(), packageVersion);
|
|
255
|
+
|
|
256
|
+
runNode([BIN, "locale", "set", "en"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
257
|
+
const localeGet = runNode([BIN, "locale", "get"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
258
|
+
assert.match(localeGet, /Effective language: en|Idioma efectivo: en/);
|
|
259
|
+
const localeDoctor = runNode([BIN, "doctor", "locale"], tempRoot, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
260
|
+
assert.match(localeDoctor, /Source: global|Origen: global/);
|
|
261
|
+
|
|
262
|
+
const splitProject = path.join(tempRoot, "split-demo");
|
|
263
|
+
fs.mkdirSync(splitProject, { recursive: true });
|
|
264
|
+
runNode([BIN, "init", "--locale", "es"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
265
|
+
|
|
266
|
+
assert.ok(fs.existsSync(path.join(splitProject, ".trackops-workspace.json")));
|
|
267
|
+
assert.ok(fs.existsSync(path.join(splitProject, ".env")));
|
|
268
|
+
assert.ok(fs.existsSync(path.join(splitProject, ".env.example")));
|
|
269
|
+
assert.ok(fs.existsSync(path.join(splitProject, "app")));
|
|
270
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "project_control.json")));
|
|
271
|
+
assert.ok(fs.existsSync(path.join(splitProject, "app", ".env")));
|
|
272
|
+
assert.ok(fs.existsSync(path.join(splitProject, "app", ".env.example")));
|
|
273
|
+
assert.ok(!fs.existsSync(path.join(splitProject, "project_control.json")));
|
|
274
|
+
assert.ok(!fs.existsSync(path.join(splitProject, "task_plan.md")));
|
|
275
|
+
|
|
276
|
+
const splitControl = readJson(path.join(splitProject, "ops", "project_control.json"));
|
|
277
|
+
assert.strictEqual(splitControl.meta.workspace.layout, "split");
|
|
278
|
+
assert.strictEqual(splitControl.meta.environment.rootEnvFile, ".env");
|
|
279
|
+
assert.strictEqual(splitControl.meta.locale, "es");
|
|
280
|
+
|
|
281
|
+
const projectLocaleDoctor = runNode([BIN, "doctor", "locale"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
282
|
+
assert.match(projectLocaleDoctor, /Project language: es|Idioma del proyecto: es/);
|
|
283
|
+
assert.match(projectLocaleDoctor, /Source: project|Origen: proyecto/);
|
|
284
|
+
|
|
273
285
|
const statusFromWorkspace = runNode([BIN, "status"], splitProject);
|
|
274
286
|
const statusFromApp = runNode([BIN, "status"], path.join(splitProject, "app"));
|
|
275
287
|
const statusFromOps = runNode([BIN, "status"], path.join(splitProject, "ops"));
|
|
276
|
-
assert.match(statusFromWorkspace, /Layout: split/);
|
|
277
|
-
assert.match(statusFromApp, /Layout: split/);
|
|
278
|
-
assert.match(statusFromOps, /Layout: split/);
|
|
288
|
+
assert.match(statusFromWorkspace, /Layout: split|Estructura: split/);
|
|
289
|
+
assert.match(statusFromApp, /Layout: split|Estructura: split/);
|
|
290
|
+
assert.match(statusFromOps, /Layout: split|Estructura: split/);
|
|
291
|
+
assert.match(statusFromWorkspace, /Git: not initialized|Git: no inicializado/i);
|
|
292
|
+
assert.doesNotMatch(statusFromWorkspace, /Branch: detached|Rama: detached/i);
|
|
279
293
|
|
|
280
294
|
const nextOutput = runNode([BIN, "next"], splitProject);
|
|
281
295
|
assert.match(nextOutput, /ops-bootstrap/);
|
|
282
296
|
|
|
297
|
+
const rerunInit = runNode([BIN, "init", "--locale", "es"], splitProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
298
|
+
assert.match(rerunInit, /Updated \.trackops-workspace\.json|Actualizado \.trackops-workspace\.json/);
|
|
299
|
+
|
|
283
300
|
runNode([BIN, "sync"], splitProject);
|
|
284
|
-
for (const file of ["task_plan.md", "progress.md", "findings.md"]) {
|
|
285
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", file)), `${file} no fue generado en ops/`);
|
|
286
|
-
}
|
|
301
|
+
for (const file of ["task_plan.md", "progress.md", "findings.md"]) {
|
|
302
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", file)), `${file} no fue generado en ops/`);
|
|
303
|
+
}
|
|
287
304
|
|
|
288
305
|
const envStatus = runNode([BIN, "env", "status"], splitProject);
|
|
289
|
-
assert.match(envStatus, /Root \.env:/);
|
|
290
|
-
assert.match(envStatus, /App bridge:/);
|
|
306
|
+
assert.match(envStatus, /Root \.env:|\.env raiz:/);
|
|
307
|
+
assert.match(envStatus, /App bridge:|Puente app:/);
|
|
291
308
|
|
|
292
309
|
const nonEmptyProject = path.join(tempRoot, "non-empty");
|
|
293
310
|
fs.mkdirSync(nonEmptyProject, { recursive: true });
|
|
294
311
|
writeJson(path.join(nonEmptyProject, "package.json"), { name: "existing-app", version: "1.0.0" });
|
|
295
|
-
const initNonEmpty =
|
|
296
|
-
assert.
|
|
297
|
-
assert.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".
|
|
310
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
311
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
312
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "
|
|
313
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
314
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
315
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-
|
|
316
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
317
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
318
|
-
assert.ok(fs.existsSync(path.join(splitProject, "ops", "
|
|
319
|
-
assert.ok(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
assert.strictEqual(operaControl.meta.
|
|
325
|
-
assert.strictEqual(operaControl.meta.opera.
|
|
326
|
-
assert.
|
|
327
|
-
assert.
|
|
328
|
-
assert.ok(operaControl.meta.opera.skills.includes("opera-
|
|
329
|
-
assert.ok(operaControl.meta.
|
|
330
|
-
|
|
331
|
-
assert.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
assert.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"
|
|
354
|
-
"
|
|
355
|
-
"--
|
|
356
|
-
"
|
|
357
|
-
"--
|
|
358
|
-
"
|
|
359
|
-
|
|
312
|
+
const initNonEmpty = runNode([BIN, "init", "--locale", "en"], nonEmptyProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
313
|
+
assert.match(initNonEmpty, /adopted into app\/|movio a app\//i);
|
|
314
|
+
assert.ok(fs.existsSync(path.join(nonEmptyProject, ".trackops-workspace.json")));
|
|
315
|
+
assert.ok(fs.existsSync(path.join(nonEmptyProject, "app", "package.json")));
|
|
316
|
+
assert.ok(fs.existsSync(path.join(nonEmptyProject, "ops", "project_control.json")));
|
|
317
|
+
|
|
318
|
+
writeJson(path.join(splitProject, "app", "package.json"), {
|
|
319
|
+
name: "split-demo",
|
|
320
|
+
version: "1.0.0",
|
|
321
|
+
dependencies: { openai: "^4.0.0" },
|
|
322
|
+
scripts: { test: "echo ok" },
|
|
323
|
+
});
|
|
324
|
+
runNode([BIN, "opera", "install", "--locale", "en", "--non-interactive"], splitProject);
|
|
325
|
+
|
|
326
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "genesis.md")));
|
|
327
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agent", "hub", "agent.md")));
|
|
328
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "_registry.md")));
|
|
329
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.md")));
|
|
330
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "agent-handoff.json")));
|
|
331
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "bootstrap", "open-questions.md")));
|
|
332
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-operations.md")));
|
|
333
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "dependency-graph.md")));
|
|
334
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "architecture", "runtime-automation.md")));
|
|
335
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "policy", "autonomy.json")));
|
|
336
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "integration-audit.md")));
|
|
337
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "reviews", "delivery-audit.md")));
|
|
338
|
+
assert.ok(!fs.existsSync(path.join(splitProject, "genesis.md")));
|
|
339
|
+
|
|
340
|
+
const operaControl = readJson(path.join(splitProject, "ops", "project_control.json"));
|
|
341
|
+
assert.strictEqual(operaControl.meta.locale, "en");
|
|
342
|
+
assert.strictEqual(operaControl.meta.opera.installed, true);
|
|
343
|
+
assert.strictEqual(operaControl.meta.opera.bootstrap.mode, "agent_handoff");
|
|
344
|
+
assert.strictEqual(operaControl.meta.opera.bootstrap.status, "awaiting_agent");
|
|
345
|
+
assert.ok(operaControl.meta.opera.skills.includes("opera-skill"));
|
|
346
|
+
assert.ok(operaControl.meta.opera.skills.includes("project-starter-skill"));
|
|
347
|
+
assert.ok(operaControl.meta.opera.skills.includes("opera-contract-auditor"));
|
|
348
|
+
assert.ok(operaControl.meta.opera.skills.includes("opera-policy-guard"));
|
|
349
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "SKILL.md")));
|
|
350
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "opera-skill", "references", "phase-dod.md")));
|
|
351
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", ".agents", "skills", "project-starter-skill", "references", "opera-cycle.md")));
|
|
352
|
+
assert.ok(operaControl.meta.environment.requiredKeys.includes("OPENAI_API_KEY"));
|
|
353
|
+
const envRootText = fs.readFileSync(path.join(splitProject, ".env"), "utf8");
|
|
354
|
+
assert.match(envRootText, /OPENAI_API_KEY=/);
|
|
355
|
+
|
|
356
|
+
const handoffPrint = runNode([BIN, "opera", "handoff", "--print"], splitProject);
|
|
357
|
+
assert.match(handoffPrint, /project-starter-skill/);
|
|
358
|
+
const handoffJson = JSON.parse(runNode([BIN, "opera", "handoff", "--json"], splitProject));
|
|
359
|
+
assert.strictEqual(handoffJson.skill, "project-starter-skill");
|
|
360
|
+
|
|
361
|
+
const directProject = path.join(tempRoot, "direct-demo");
|
|
362
|
+
fs.mkdirSync(directProject, { recursive: true });
|
|
363
|
+
runNode([BIN, "init", "--locale", "en"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
364
|
+
writeJson(path.join(directProject, "app", "package.json"), { name: "direct-demo", version: "1.0.0" });
|
|
365
|
+
runNode([
|
|
366
|
+
BIN,
|
|
367
|
+
"opera",
|
|
368
|
+
"install",
|
|
369
|
+
"--locale",
|
|
370
|
+
"en",
|
|
371
|
+
"--non-interactive",
|
|
372
|
+
"--bootstrap-mode",
|
|
373
|
+
"direct",
|
|
374
|
+
"--technical-level",
|
|
375
|
+
"senior",
|
|
376
|
+
"--project-state",
|
|
377
|
+
"existing_repo",
|
|
378
|
+
"--docs-state",
|
|
379
|
+
"spec_dossier",
|
|
380
|
+
"--decision-ownership",
|
|
381
|
+
"user",
|
|
382
|
+
], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
360
383
|
const directControl = readJson(path.join(directProject, "ops", "project_control.json"));
|
|
361
384
|
assert.strictEqual(directControl.meta.opera.bootstrap.mode, "direct_cli");
|
|
362
385
|
assert.strictEqual(directControl.meta.opera.bootstrap.status, "awaiting_intake");
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
assert.
|
|
438
|
-
assert.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
assert.
|
|
449
|
-
assert.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
assert.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
386
|
+
assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "intake.json")));
|
|
387
|
+
assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "spec-dossier.md")));
|
|
388
|
+
assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "open-questions.md")));
|
|
389
|
+
assert.ok(fs.existsSync(path.join(directProject, "ops", "bootstrap", "quality-report.json")));
|
|
390
|
+
const directOperaStatus = runNode([BIN, "opera", "status"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
391
|
+
assert.match(directOperaStatus, /Intake:|Intake:/);
|
|
392
|
+
assert.match(directOperaStatus, /Spec dossier:|Specification brief:|Dossier de especificacion:/i);
|
|
393
|
+
assert.doesNotMatch(directOperaStatus, /The agent did not include|El agente no incluyo/i);
|
|
394
|
+
assert.doesNotMatch(directOperaStatus, /Handoff: ops[\\/]+bootstrap[\\/]+agent-handoff\.md/i);
|
|
395
|
+
const directHandoffSummary = runNode([BIN, "opera", "handoff"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
396
|
+
assert.match(directHandoffSummary, /Guided bootstrap summary|Resumen del bootstrap guiado/i);
|
|
397
|
+
assert.doesNotMatch(directHandoffSummary, /Markdown handoff/i);
|
|
398
|
+
|
|
399
|
+
const plainStatus = runNode([BIN, "--plain", "status"], directProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
400
|
+
assert.match(plainStatus, /\[BLOCKED\]|\[PENDING\]/);
|
|
401
|
+
assert.doesNotMatch(plainStatus, /\u2500|\u23F3|\u26D4|\u2705/);
|
|
402
|
+
|
|
403
|
+
const promptProject = path.join(tempRoot, "prompt-demo");
|
|
404
|
+
fs.mkdirSync(promptProject, { recursive: true });
|
|
405
|
+
runNode([BIN, "init", "--locale", "en"], promptProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
406
|
+
writeJson(path.join(promptProject, "app", "package.json"), { name: "prompt-demo", version: "1.0.0" });
|
|
407
|
+
runNodeWithInput([BIN, "opera", "install", "--locale", "en"], promptProject, "\n", { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
408
|
+
assert.ok(!fs.existsSync(path.join(promptProject, "ops", "bootstrap", "agent-handoff.md")));
|
|
409
|
+
|
|
410
|
+
writeJson(path.join(splitProject, "ops", "bootstrap", "intake.json"), {
|
|
411
|
+
version: 1,
|
|
412
|
+
technicalLevel: "low",
|
|
413
|
+
projectState: "idea",
|
|
414
|
+
documentationState: "none",
|
|
415
|
+
decisionOwnership: "agent",
|
|
416
|
+
problemStatement: "users need a simple way to book and pay online",
|
|
417
|
+
targetUser: "small studio owners",
|
|
418
|
+
singularDesiredOutcome: "let users create and pay for bookings",
|
|
419
|
+
userLanguage: "en",
|
|
420
|
+
needsPlainLanguage: true,
|
|
421
|
+
recommendedStack: ["nextjs"],
|
|
422
|
+
externalServices: ["OpenAI", "Stripe"],
|
|
423
|
+
sourceOfTruth: "primary bookings database",
|
|
424
|
+
payload: "bookings dashboard",
|
|
425
|
+
behaviorRules: ["keep explanations simple"],
|
|
426
|
+
architecturalInvariants: ["keep app and ops separated"],
|
|
427
|
+
inputSchema: { booking: { email: "string" } },
|
|
428
|
+
outputSchema: { confirmation: { id: "string" } },
|
|
429
|
+
pipeline: ["create booking", "confirm payment"],
|
|
430
|
+
templates: ["booking-confirmation"],
|
|
431
|
+
});
|
|
432
|
+
fs.writeFileSync(
|
|
433
|
+
path.join(splitProject, "ops", "bootstrap", "spec-dossier.md"),
|
|
434
|
+
"# Spec dossier\n\n## Problem statement\nusers need a simple way to book and pay online\n\n## Target user\nsmall studio owners\n\n## Singular desired outcome\nlet users create and pay for bookings\n\n## Delivery target\nbookings dashboard\n\n## Source of truth\nprimary bookings database\n",
|
|
435
|
+
"utf8",
|
|
436
|
+
);
|
|
437
|
+
runNode([BIN, "opera", "bootstrap", "--resume"], splitProject);
|
|
438
|
+
const resumedControl = readJson(path.join(splitProject, "ops", "project_control.json"));
|
|
439
|
+
assert.strictEqual(resumedControl.meta.opera.bootstrap.status, "completed");
|
|
440
|
+
assert.strictEqual(resumedControl.meta.currentFocus, "let users create and pay for bookings");
|
|
441
|
+
assert.ok(resumedControl.meta.environment.requiredKeys.includes("STRIPE_SECRET_KEY"));
|
|
442
|
+
assert.ok(fs.existsSync(path.join(splitProject, "ops", "contract", "operating-contract.json")));
|
|
443
|
+
const operatingContract = readJson(path.join(splitProject, "ops", "contract", "operating-contract.json"));
|
|
444
|
+
assert.strictEqual(operatingContract.version, 3);
|
|
445
|
+
assert.strictEqual(operatingContract.userModel.language, "en");
|
|
446
|
+
assert.strictEqual(operatingContract.userModel.decisionOwnership, "agent");
|
|
447
|
+
|
|
448
|
+
const defaultDashboard = startDashboard(splitProject);
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const ready = await waitForDashboard(defaultDashboard);
|
|
452
|
+
const state = await get(ready.port, "/api/state");
|
|
453
|
+
const envPayload = await get(ready.port, "/api/env");
|
|
454
|
+
const operaBootstrapPayload = await get(ready.port, "/api/opera/bootstrap");
|
|
455
|
+
const operaHandoffPayload = await get(ready.port, "/api/opera/handoff");
|
|
456
|
+
const localSkills = await get(ready.port, "/api/skills/local");
|
|
457
|
+
const discoverSkills = await get(ready.port, "/api/skills/discover");
|
|
458
|
+
|
|
459
|
+
assert.strictEqual(state.status, 200);
|
|
460
|
+
assert.strictEqual(envPayload.status, 200);
|
|
461
|
+
assert.strictEqual(operaBootstrapPayload.status, 200);
|
|
462
|
+
assert.strictEqual(operaHandoffPayload.status, 200);
|
|
463
|
+
assert.strictEqual(localSkills.status, 200);
|
|
464
|
+
assert.strictEqual(discoverSkills.status, 200);
|
|
465
|
+
|
|
466
|
+
const statePayload = JSON.parse(state.body);
|
|
467
|
+
assert.strictEqual(statePayload.project.layout, "split");
|
|
468
|
+
assert.strictEqual(statePayload.project.workspaceRoot, splitProject);
|
|
469
|
+
assert.strictEqual(statePayload.project.appRoot, path.join(splitProject, "app"));
|
|
470
|
+
assert.strictEqual(statePayload.project.opsRoot, path.join(splitProject, "ops"));
|
|
471
|
+
assert.ok(Array.isArray(statePayload.env.requiredKeys));
|
|
472
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(statePayload.env, "values"));
|
|
473
|
+
|
|
474
|
+
const envState = JSON.parse(envPayload.body);
|
|
475
|
+
assert.ok(envState.requiredKeys.includes("OPENAI_API_KEY"));
|
|
476
|
+
assert.ok(envState.missingKeys.includes("OPENAI_API_KEY"));
|
|
477
|
+
|
|
478
|
+
const bootstrapState = JSON.parse(operaBootstrapPayload.body);
|
|
479
|
+
assert.strictEqual(bootstrapState.status, "completed");
|
|
480
|
+
assert.strictEqual(bootstrapState.contractVersion, 3);
|
|
481
|
+
assert.strictEqual(bootstrapState.contractReadiness, "verified");
|
|
482
|
+
const handoffState = JSON.parse(operaHandoffPayload.body);
|
|
483
|
+
assert.ok(handoffState.markdown.includes("project-starter-skill"));
|
|
484
|
+
assert.ok(handoffState.openQuestionsFile.endsWith("open-questions.md"));
|
|
485
|
+
|
|
486
|
+
} finally {
|
|
487
|
+
await stopDashboard(defaultDashboard);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const blocked4173 = await isPortFree(4173) ? await occupyPort(4173) : null;
|
|
491
|
+
const fallbackDashboard = startDashboard(splitProject);
|
|
492
|
+
try {
|
|
493
|
+
const ready = await waitForDashboard(fallbackDashboard);
|
|
494
|
+
assert.notStrictEqual(ready.port, 4173, `deberia haber evitado 4173 ocupado:\n${ready.output}`);
|
|
495
|
+
assert.match(ready.output, /Local:/);
|
|
496
|
+
} finally {
|
|
497
|
+
await stopDashboard(fallbackDashboard);
|
|
498
|
+
if (blocked4173) blocked4173.close();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const strictPort = await findFreePort();
|
|
502
|
+
const occupiedStrictPort = await occupyPort(strictPort);
|
|
503
|
+
try {
|
|
504
|
+
const strictResult = runNodeResult([BIN, "dashboard", "--strict-port", "--port", String(strictPort)], splitProject);
|
|
505
|
+
assert.notStrictEqual(strictResult.status, 0, "el dashboard deberia fallar con --strict-port si el puerto esta ocupado");
|
|
506
|
+
assert.match(`${strictResult.stdout}\n${strictResult.stderr}`, /already in use|esta en uso/i);
|
|
507
|
+
} finally {
|
|
508
|
+
occupiedStrictPort.close();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const publicPort = await findFreePort("0.0.0.0");
|
|
512
|
+
const publicDashboard = startDashboard(splitProject, ["--public", "--port", String(publicPort)]);
|
|
513
|
+
try {
|
|
514
|
+
const ready = await waitForDashboard(publicDashboard);
|
|
515
|
+
if (hasExternalIpv4()) {
|
|
516
|
+
assert.match(ready.output, /- Network:/);
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
await stopDashboard(publicDashboard);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const noClipboardPort = await findFreePort();
|
|
523
|
+
const noClipboardDashboard = startDashboard(splitProject, ["--port", String(noClipboardPort)], {
|
|
524
|
+
PATH: "",
|
|
525
|
+
Path: "",
|
|
526
|
+
});
|
|
527
|
+
try {
|
|
528
|
+
const ready = await waitForDashboard(noClipboardDashboard);
|
|
529
|
+
const state = await get(ready.port, "/api/state");
|
|
530
|
+
assert.strictEqual(state.status, 200);
|
|
531
|
+
} finally {
|
|
532
|
+
await stopDashboard(noClipboardDashboard);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const legacyProject = path.join(tempRoot, "legacy-demo");
|
|
536
|
+
fs.mkdirSync(legacyProject, { recursive: true });
|
|
537
|
+
initGitRepo(legacyProject);
|
|
538
|
+
writeJson(path.join(legacyProject, "package.json"), { name: "legacy-demo", version: "1.0.0" });
|
|
539
|
+
runNode([BIN, "init", "--legacy-layout"], legacyProject);
|
|
540
|
+
runNode([BIN, "opera", "install", "--locale", "en", "--no-bootstrap"], legacyProject);
|
|
541
|
+
fs.writeFileSync(path.join(legacyProject, ".env"), "OPENAI_API_KEY=\n", "utf8");
|
|
542
|
+
fs.writeFileSync(path.join(legacyProject, ".env.example"), "OPENAI_API_KEY=\n", "utf8");
|
|
543
|
+
commitAll(legacyProject, "legacy fixture");
|
|
544
|
+
|
|
545
|
+
runNode([BIN, "workspace", "migrate"], legacyProject);
|
|
546
|
+
assert.ok(fs.existsSync(path.join(legacyProject, ".trackops-workspace.json")));
|
|
547
|
+
assert.ok(fs.existsSync(path.join(legacyProject, "app", "package.json")));
|
|
548
|
+
assert.ok(fs.existsSync(path.join(legacyProject, "ops", "project_control.json")));
|
|
549
|
+
assert.ok(fs.existsSync(path.join(legacyProject, ".env")));
|
|
550
|
+
assert.ok(fs.existsSync(path.join(legacyProject, "app", ".env")));
|
|
551
|
+
assert.ok(!fs.existsSync(path.join(legacyProject, "project_control.json")));
|
|
552
|
+
const migratedPackage = readJson(path.join(legacyProject, "app", "package.json"));
|
|
553
|
+
assert.ok(!migratedPackage.scripts || !migratedPackage.scripts["ops:status"]);
|
|
554
|
+
assert.match(git(["branch", "--list"], legacyProject), /backup\/trackops-workspace-/);
|
|
555
|
+
|
|
556
|
+
const legacyUnsupportedProject = path.join(tempRoot, "legacy-unsupported");
|
|
557
|
+
fs.mkdirSync(legacyUnsupportedProject, { recursive: true });
|
|
558
|
+
initGitRepo(legacyUnsupportedProject);
|
|
559
|
+
runNode([BIN, "init", "--locale", "en"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
560
|
+
const legacyUnsupportedControlPath = path.join(legacyUnsupportedProject, "ops", "project_control.json");
|
|
561
|
+
const legacyUnsupportedControl = readJson(legacyUnsupportedControlPath);
|
|
562
|
+
legacyUnsupportedControl.meta.opera = {
|
|
563
|
+
installed: true,
|
|
564
|
+
version: "0.9.0",
|
|
565
|
+
};
|
|
566
|
+
writeJson(legacyUnsupportedControlPath, legacyUnsupportedControl);
|
|
521
567
|
const legacyStatusOutput = runNode([BIN, "opera", "status"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
522
568
|
assert.match(legacyStatusOutput, /legacy_unsupported/);
|
|
523
|
-
const upgradeWithoutReset = runNodeResult([BIN, "opera", "upgrade", "--stable"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
524
|
-
assert.strictEqual(upgradeWithoutReset.status, 0);
|
|
525
|
-
assert.match(`${upgradeWithoutReset.stdout}\n${upgradeWithoutReset.stderr}`, /legacy/i);
|
|
526
|
-
runNode([BIN, "opera", "upgrade", "--stable", "--reset"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
527
|
-
const upgradedLegacyControl = readJson(legacyUnsupportedControlPath);
|
|
528
|
-
assert.strictEqual(upgradedLegacyControl.meta.opera.legacyStatus, "supported");
|
|
529
|
-
assert.strictEqual(upgradedLegacyControl.meta.opera.stableTag, "stable");
|
|
530
|
-
assert.ok(fs.existsSync(path.join(legacyUnsupportedProject, "ops", ".tmp", "upgrade-backups")));
|
|
531
|
-
|
|
532
|
-
const releaseProject = path.join(tempRoot, "release-demo");
|
|
533
|
-
fs.mkdirSync(releaseProject, { recursive: true });
|
|
534
|
-
initGitRepo(releaseProject);
|
|
535
|
-
runNode([BIN, "init"], releaseProject);
|
|
536
|
-
writeJson(path.join(releaseProject, "app", "package.json"), { name: "release-demo", version: "1.0.0" });
|
|
537
|
-
fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
|
|
538
|
-
commitAll(releaseProject, "split fixture");
|
|
539
|
-
git(["checkout", "-b", "develop"], releaseProject);
|
|
540
|
-
runNode([BIN, "release"], releaseProject);
|
|
541
|
-
const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
|
|
542
|
-
assert.ok(publishFiles.includes("package.json"));
|
|
543
|
-
assert.ok(publishFiles.includes(".env.example"));
|
|
544
|
-
assert.ok(!publishFiles.includes("ops"));
|
|
545
|
-
assert.ok(!publishFiles.includes(".trackops-workspace.json"));
|
|
546
|
-
|
|
547
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
548
|
-
console.log("Smoke tests OK");
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
main().catch((error) => {
|
|
552
|
-
console.error(error.message);
|
|
553
|
-
process.exit(1);
|
|
554
|
-
});
|
|
569
|
+
const upgradeWithoutReset = runNodeResult([BIN, "opera", "upgrade", "--stable"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
570
|
+
assert.strictEqual(upgradeWithoutReset.status, 0);
|
|
571
|
+
assert.match(`${upgradeWithoutReset.stdout}\n${upgradeWithoutReset.stderr}`, /legacy/i);
|
|
572
|
+
runNode([BIN, "opera", "upgrade", "--stable", "--reset"], legacyUnsupportedProject, { TRACKOPS_BOOTSTRAP_HOME: bootstrapHome });
|
|
573
|
+
const upgradedLegacyControl = readJson(legacyUnsupportedControlPath);
|
|
574
|
+
assert.strictEqual(upgradedLegacyControl.meta.opera.legacyStatus, "supported");
|
|
575
|
+
assert.strictEqual(upgradedLegacyControl.meta.opera.stableTag, "stable");
|
|
576
|
+
assert.ok(fs.existsSync(path.join(legacyUnsupportedProject, "ops", ".tmp", "upgrade-backups")));
|
|
577
|
+
|
|
578
|
+
const releaseProject = path.join(tempRoot, "release-demo");
|
|
579
|
+
fs.mkdirSync(releaseProject, { recursive: true });
|
|
580
|
+
initGitRepo(releaseProject);
|
|
581
|
+
runNode([BIN, "init"], releaseProject);
|
|
582
|
+
writeJson(path.join(releaseProject, "app", "package.json"), { name: "release-demo", version: "1.0.0" });
|
|
583
|
+
fs.writeFileSync(path.join(releaseProject, "app", "index.js"), "console.log('release');\n", "utf8");
|
|
584
|
+
commitAll(releaseProject, "split fixture");
|
|
585
|
+
git(["checkout", "-b", "develop"], releaseProject);
|
|
586
|
+
runNode([BIN, "release"], releaseProject);
|
|
587
|
+
const publishFiles = git(["ls-tree", "--name-only", "master"], releaseProject).split(/\r?\n/).filter(Boolean);
|
|
588
|
+
assert.ok(publishFiles.includes("package.json"));
|
|
589
|
+
assert.ok(publishFiles.includes(".env.example"));
|
|
590
|
+
assert.ok(!publishFiles.includes("ops"));
|
|
591
|
+
assert.ok(!publishFiles.includes(".trackops-workspace.json"));
|
|
592
|
+
|
|
593
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
594
|
+
console.log("Smoke tests OK");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
main().catch((error) => {
|
|
598
|
+
console.error(error.message);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
});
|