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/lib/control.js
CHANGED
|
@@ -4,21 +4,22 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { spawnSync } = require("child_process");
|
|
6
6
|
|
|
7
|
-
const config = require("./config");
|
|
8
|
-
const env = require("./env");
|
|
9
|
-
const { t, setLocale, getLocale } = require("./i18n");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
const config = require("./config");
|
|
8
|
+
const env = require("./env");
|
|
9
|
+
const { t, setLocale, getLocale } = require("./i18n");
|
|
10
|
+
const fmt = require("./cli-format");
|
|
11
|
+
|
|
12
|
+
const PRIORITY_ORDER = ["P0", "P1", "P2", "P3"];
|
|
13
|
+
const STATUS_ORDER = ["in_progress", "in_review", "pending", "blocked", "completed", "cancelled"];
|
|
14
|
+
const STATUS_ICONS = {
|
|
15
|
+
pending: "\u23F3",
|
|
16
|
+
in_progress: "\uD83D\uDEA7",
|
|
17
|
+
in_review: "\uD83D\uDC40",
|
|
18
|
+
blocked: "\u26D4",
|
|
19
|
+
completed: "\u2705",
|
|
20
|
+
cancelled: "\uD83D\uDDD1\uFE0F",
|
|
21
|
+
};
|
|
22
|
+
const CHECK_ICONS = {
|
|
22
23
|
pass: "\u2705",
|
|
23
24
|
warn: "\u26A0\uFE0F",
|
|
24
25
|
fail: "\u274C",
|
|
@@ -39,29 +40,79 @@ function nowIso() {
|
|
|
39
40
|
return new Date().toISOString();
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function git(args, root) {
|
|
43
|
-
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
44
|
-
if (result.error || result.status !== 0) return null;
|
|
45
|
-
return result.stdout.replace(/\s+$/, "");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
function git(args, root) {
|
|
44
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
45
|
+
if (result.error || result.status !== 0) return null;
|
|
46
|
+
return result.stdout.replace(/\s+$/, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function gitResult(args, root) {
|
|
50
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
|
|
51
|
+
return {
|
|
52
|
+
ok: !result.error && result.status === 0,
|
|
53
|
+
status: result.status,
|
|
54
|
+
stdout: String(result.stdout || "").replace(/\s+$/, ""),
|
|
55
|
+
stderr: String(result.stderr || "").replace(/\s+$/, ""),
|
|
56
|
+
error: result.error || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function statusLabel(status) {
|
|
61
|
+
return t(`status.${status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatBootstrapStatus(status) {
|
|
65
|
+
return t(`bootstrap.status.${String(status || "").trim()}`) || status || t("locale.none");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatBootstrapMode(mode) {
|
|
69
|
+
return t(`bootstrap.mode.${String(mode || "").trim()}`) || mode || t("locale.none");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatBootstrapReason(reason) {
|
|
73
|
+
return t(`bootstrap.reason.${String(reason || "").trim()}`) || reason || t("locale.none");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function listOrNone(items) {
|
|
77
|
+
const values = (items || []).filter(Boolean);
|
|
78
|
+
return values.length ? values.join(", ") : t("locale.none");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── repo snapshot ── */
|
|
82
|
+
|
|
83
|
+
function getRepoSnapshot(contextOrRoot) {
|
|
84
|
+
const context = config.ensureContext(contextOrRoot);
|
|
85
|
+
const repoRoot = context.workspaceRoot;
|
|
86
|
+
const insideWorkTree = gitResult(["rev-parse", "--is-inside-work-tree"], repoRoot);
|
|
87
|
+
if (!insideWorkTree.ok || insideWorkTree.stdout.trim() !== "true") {
|
|
88
|
+
return {
|
|
89
|
+
generatedAt: nowIso(),
|
|
90
|
+
available: false,
|
|
91
|
+
state: "not_initialized",
|
|
92
|
+
branch: null,
|
|
93
|
+
clean: null,
|
|
94
|
+
staged: 0,
|
|
95
|
+
unstaged: 0,
|
|
96
|
+
untracked: 0,
|
|
97
|
+
ahead: 0,
|
|
98
|
+
behind: 0,
|
|
99
|
+
hasUpstream: false,
|
|
100
|
+
lastCommit: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const branchResult = gitResult(["branch", "--show-current"], repoRoot);
|
|
105
|
+
const branch = branchResult.ok ? branchResult.stdout.trim() : "";
|
|
106
|
+
const status = git(["status", "--short"], repoRoot) || "";
|
|
107
|
+
const lines = status.split(/\r?\n/).filter(Boolean);
|
|
108
|
+
const lastCommitRaw = git(["log", "-1", "--pretty=format:%H%n%cs%n%s"], repoRoot);
|
|
109
|
+
const upstream = gitResult(["rev-parse", "--abbrev-ref", "@{upstream}"], repoRoot);
|
|
110
|
+
const divergenceRaw = upstream.ok
|
|
111
|
+
? git(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], repoRoot)
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
let staged = 0;
|
|
115
|
+
let unstaged = 0;
|
|
65
116
|
let untracked = 0;
|
|
66
117
|
|
|
67
118
|
lines.forEach((line) => {
|
|
@@ -78,14 +129,27 @@ function getRepoSnapshot(contextOrRoot) {
|
|
|
78
129
|
|
|
79
130
|
let ahead = 0;
|
|
80
131
|
let behind = 0;
|
|
81
|
-
if (divergenceRaw) {
|
|
82
|
-
const [left, right] = divergenceRaw.split(/\s+/).map(Number);
|
|
83
|
-
behind = Number.isFinite(left) ? left : 0;
|
|
84
|
-
ahead = Number.isFinite(right) ? right : 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
|
|
132
|
+
if (divergenceRaw) {
|
|
133
|
+
const [left, right] = divergenceRaw.split(/\s+/).map(Number);
|
|
134
|
+
behind = Number.isFinite(left) ? left : 0;
|
|
135
|
+
ahead = Number.isFinite(right) ? right : 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
generatedAt: nowIso(),
|
|
140
|
+
available: true,
|
|
141
|
+
state: branch ? "ready" : "detached",
|
|
142
|
+
branch: branch || null,
|
|
143
|
+
clean: lines.length === 0,
|
|
144
|
+
staged,
|
|
145
|
+
unstaged,
|
|
146
|
+
untracked,
|
|
147
|
+
ahead,
|
|
148
|
+
behind,
|
|
149
|
+
hasUpstream: upstream.ok,
|
|
150
|
+
lastCommit,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
89
153
|
|
|
90
154
|
function refreshRepoRuntime(root, options = {}) {
|
|
91
155
|
const context = config.ensureContext(root);
|
|
@@ -115,16 +179,42 @@ function compareTasks(a, b, phases) {
|
|
|
115
179
|
return a.title.localeCompare(b.title, getLocale());
|
|
116
180
|
}
|
|
117
181
|
|
|
182
|
+
function detectCircularDeps(tasks) {
|
|
183
|
+
const visited = new Set();
|
|
184
|
+
const inStack = new Set();
|
|
185
|
+
const cycles = [];
|
|
186
|
+
function dfs(id) {
|
|
187
|
+
if (inStack.has(id)) { cycles.push(id); return; }
|
|
188
|
+
if (visited.has(id)) return;
|
|
189
|
+
visited.add(id);
|
|
190
|
+
inStack.add(id);
|
|
191
|
+
const task = tasks.find((t) => t.id === id);
|
|
192
|
+
if (task) (task.dependsOn || []).forEach((dep) => dfs(dep));
|
|
193
|
+
inStack.delete(id);
|
|
194
|
+
}
|
|
195
|
+
tasks.forEach((t) => dfs(t.id));
|
|
196
|
+
return cycles;
|
|
197
|
+
}
|
|
198
|
+
|
|
118
199
|
function derive(control) {
|
|
119
200
|
const phases = config.getPhases(control);
|
|
120
201
|
const tasks = [...control.tasks].sort((a, b) => compareTasks(a, b, phases));
|
|
121
202
|
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
203
|
+
const allIds = new Set(tasks.map((t) => t.id));
|
|
122
204
|
const closedStatuses = new Set(["completed", "cancelled"]);
|
|
205
|
+
const phantomDeps = [];
|
|
123
206
|
|
|
124
207
|
const readyTasks = tasks
|
|
125
208
|
.filter((task) => {
|
|
126
209
|
if (task.status !== "pending") return false;
|
|
127
|
-
|
|
210
|
+
const validDeps = (task.dependsOn || []).filter((dep) => {
|
|
211
|
+
if (!allIds.has(dep)) {
|
|
212
|
+
phantomDeps.push({ taskId: task.id, missingDep: dep });
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
return validDeps.every((dep) => completedIds.has(dep));
|
|
128
218
|
})
|
|
129
219
|
.sort((a, b) => {
|
|
130
220
|
const focusPhase = control.meta.focusPhase || "";
|
|
@@ -139,10 +229,11 @@ function derive(control) {
|
|
|
139
229
|
const reviewTasks = tasks.filter((t) => t.status === "in_review");
|
|
140
230
|
const openTasks = tasks.filter((t) => !["completed", "cancelled"].includes(t.status));
|
|
141
231
|
const requiredOpenTasks = tasks.filter((t) => t.required !== false && !["completed", "cancelled"].includes(t.status));
|
|
232
|
+
const projectCompleted = requiredOpenTasks.length === 0 && tasks.length > 0;
|
|
142
233
|
|
|
143
234
|
const activePhase =
|
|
144
235
|
phases.find((p) => requiredOpenTasks.some((t) => t.phase === p.id)) ||
|
|
145
|
-
phases[phases.length - 1];
|
|
236
|
+
(projectCompleted ? phases[phases.length - 1] : phases[0]);
|
|
146
237
|
|
|
147
238
|
const phaseStats = phases.map((phase) => {
|
|
148
239
|
const phaseTasks = tasks.filter((t) => t.phase === phase.id && t.required !== false);
|
|
@@ -152,9 +243,13 @@ function derive(control) {
|
|
|
152
243
|
});
|
|
153
244
|
|
|
154
245
|
const nextTask = activeTasks[0] || readyTasks[0] || blockers[0] || openTasks[0] || null;
|
|
246
|
+
const circularDeps = detectCircularDeps(tasks);
|
|
155
247
|
|
|
156
248
|
return {
|
|
157
249
|
tasks, blockers, activeTasks, reviewTasks, readyTasks, nextTask, activePhase, phaseStats,
|
|
250
|
+
projectCompleted,
|
|
251
|
+
circularDeps,
|
|
252
|
+
phantomDeps,
|
|
158
253
|
openFindings: (control.findings || []).filter((f) => f.status === "open"),
|
|
159
254
|
resolvedFindings: (control.findings || []).filter((f) => f.status === "resolved"),
|
|
160
255
|
totals: {
|
|
@@ -171,12 +266,13 @@ function derive(control) {
|
|
|
171
266
|
|
|
172
267
|
/* ── render ── */
|
|
173
268
|
|
|
174
|
-
function renderTask(task, phases) {
|
|
175
|
-
const phase = getPhaseInfo(task.phase, phases);
|
|
176
|
-
const detail = task.blocker || task.summary || "";
|
|
177
|
-
const detailSuffix = detail ? ` — ${detail}` : "";
|
|
178
|
-
|
|
179
|
-
}
|
|
269
|
+
function renderTask(task, phases, options = {}) {
|
|
270
|
+
const phase = getPhaseInfo(task.phase, phases);
|
|
271
|
+
const detail = task.blocker || task.summary || "";
|
|
272
|
+
const detailSuffix = detail ? ` — ${detail}` : "";
|
|
273
|
+
const token = options.cli ? fmt.statusToken(task.status) : STATUS_ICONS[task.status];
|
|
274
|
+
return `- ${token} \`${task.id}\` [${task.priority}] ${task.title} (${phase.id} · ${phase.label} · ${task.stream})${detailSuffix}`;
|
|
275
|
+
}
|
|
180
276
|
|
|
181
277
|
function renderTaskPlan(control) {
|
|
182
278
|
const phases = config.getPhases(control);
|
|
@@ -284,7 +380,7 @@ function renderProgress(control) {
|
|
|
284
380
|
`## ${t("doc.section.currentState")}`,
|
|
285
381
|
`- ${t("doc.label.phase")}: ${state.activePhase.label} (${state.activePhase.index}/${phases.length})`,
|
|
286
382
|
`- ${t("doc.label.blockers")}: ${blockersLabel}`,
|
|
287
|
-
`- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
|
|
383
|
+
`- ${t("doc.label.lastTest")}: ${CHECK_ICONS[lastTest.status]} ${lastTest.date || t("status.pending")}${lastTest.note ? ` — ${lastTest.note}` : ""}`,
|
|
288
384
|
`- ${t("doc.label.nextStepShort")}: ${nextStep}`,
|
|
289
385
|
`- ${t("doc.label.lastUpdate")}: ${(control.meta.updatedAt || "").slice(0, 10)}`,
|
|
290
386
|
"",
|
|
@@ -371,13 +467,26 @@ function getDocDrift(root, control) {
|
|
|
371
467
|
.map(([name]) => name);
|
|
372
468
|
}
|
|
373
469
|
|
|
374
|
-
function syncDocs(root, control) {
|
|
470
|
+
function syncDocs(root, control, options = {}) {
|
|
375
471
|
const context = config.ensureContext(root);
|
|
376
472
|
const docs = buildDocMap(control);
|
|
377
473
|
const docFiles = config.docFilePaths(context);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
474
|
+
const pairs = [
|
|
475
|
+
[docFiles.taskPlan, docs.taskPlan],
|
|
476
|
+
[docFiles.progress, docs.progress],
|
|
477
|
+
[docFiles.findings, docs.findings],
|
|
478
|
+
];
|
|
479
|
+
for (const [filePath, content] of pairs) {
|
|
480
|
+
if (!options.force && fs.existsSync(filePath)) {
|
|
481
|
+
const existing = fs.readFileSync(filePath, "utf8");
|
|
482
|
+
const isAutoGenerated = existing.includes(t("doc.autogenerated"));
|
|
483
|
+
if (!isAutoGenerated && existing.trim()) {
|
|
484
|
+
console.log(t("control.docsOverwriteWarning", { file: path.basename(filePath) }));
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
writeText(filePath, `${content}\n`);
|
|
489
|
+
}
|
|
381
490
|
}
|
|
382
491
|
|
|
383
492
|
/* ── task management ── */
|
|
@@ -396,6 +505,10 @@ function updateTask(root, control, action, taskId, note) {
|
|
|
396
505
|
const nextStatus = actionMap[action];
|
|
397
506
|
if (!nextStatus) throw new Error(t("cli.actionNotSupported", { action }));
|
|
398
507
|
|
|
508
|
+
if (task.status === nextStatus) {
|
|
509
|
+
console.log(t("control.taskAlreadyStatus", { taskId, status: nextStatus }));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
399
512
|
task.status = nextStatus;
|
|
400
513
|
if (nextStatus === "blocked") {
|
|
401
514
|
task.blocker = note || task.blocker || t("cli.undocumentedBlocker");
|
|
@@ -432,66 +545,77 @@ function cmdStatus(root) {
|
|
|
432
545
|
const drift = getDocDrift(context, control);
|
|
433
546
|
const envAudit = env.auditEnvironment(context, control);
|
|
434
547
|
|
|
435
|
-
console.log(t("cli.status.title", { projectName: control.meta.projectName }));
|
|
436
|
-
console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
|
|
437
|
-
console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
|
|
438
|
-
console.log(
|
|
439
|
-
if (context.layout === "split") {
|
|
440
|
-
console.log(
|
|
441
|
-
console.log(
|
|
442
|
-
}
|
|
443
|
-
if (control.meta?.opera?.bootstrap?.status) {
|
|
444
|
-
console.log(t("cli.status.bootstrap", {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
if (control.meta.opera.bootstrap.
|
|
449
|
-
console.log(
|
|
450
|
-
}
|
|
451
|
-
|
|
548
|
+
console.log(t("cli.status.title", { projectName: control.meta.projectName }));
|
|
549
|
+
console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
|
|
550
|
+
console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
|
|
551
|
+
console.log(t("cli.status.layout", { layout: context.layout, workspace: context.workspaceRoot }));
|
|
552
|
+
if (context.layout === "split") {
|
|
553
|
+
console.log(t("cli.status.appRoot", { path: context.appRoot }));
|
|
554
|
+
console.log(t("cli.status.opsRoot", { path: context.opsRoot }));
|
|
555
|
+
}
|
|
556
|
+
if (control.meta?.opera?.bootstrap?.status) {
|
|
557
|
+
console.log(t("cli.status.bootstrap", {
|
|
558
|
+
status: formatBootstrapStatus(control.meta.opera.bootstrap.status),
|
|
559
|
+
locale: config.getLocale(control),
|
|
560
|
+
}));
|
|
561
|
+
if (control.meta.opera.bootstrap.mode) {
|
|
562
|
+
console.log(t("cli.status.bootstrapMode", { value: formatBootstrapMode(control.meta.opera.bootstrap.mode) }));
|
|
563
|
+
}
|
|
564
|
+
if (control.meta.opera.bootstrap.routeReason) {
|
|
565
|
+
console.log(t("cli.status.bootstrapReason", { value: formatBootstrapReason(control.meta.opera.bootstrap.routeReason) }));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
452
568
|
console.log(t("cli.status.tasks", {
|
|
453
569
|
completed: state.totals.completed, inProgress: state.totals.inProgress,
|
|
454
570
|
inReview: state.totals.inReview, pending: state.totals.pending, blocked: state.totals.blocked,
|
|
455
571
|
}));
|
|
456
572
|
console.log("");
|
|
457
|
-
console.log(t("cli.status.readyTasks"));
|
|
458
|
-
if (state.readyTasks.length) {
|
|
459
|
-
state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases)));
|
|
460
|
-
} else {
|
|
461
|
-
console.log(t("cli.status.noReadyTasks"));
|
|
462
|
-
}
|
|
573
|
+
console.log(t("cli.status.readyTasks"));
|
|
574
|
+
if (state.readyTasks.length) {
|
|
575
|
+
state.readyTasks.slice(0, 5).forEach((task) => console.log(renderTask(task, phases, { cli: true })));
|
|
576
|
+
} else {
|
|
577
|
+
console.log(t("cli.status.noReadyTasks"));
|
|
578
|
+
}
|
|
463
579
|
console.log("");
|
|
464
|
-
console.log(t("cli.status.blockers"));
|
|
465
|
-
if (state.blockers.length) {
|
|
466
|
-
state.blockers.forEach((task) => console.log(renderTask(task, phases)));
|
|
467
|
-
} else {
|
|
468
|
-
console.log(t("cli.status.noBlockers"));
|
|
469
|
-
}
|
|
580
|
+
console.log(t("cli.status.blockers"));
|
|
581
|
+
if (state.blockers.length) {
|
|
582
|
+
state.blockers.forEach((task) => console.log(renderTask(task, phases, { cli: true })));
|
|
583
|
+
} else {
|
|
584
|
+
console.log(t("cli.status.noBlockers"));
|
|
585
|
+
}
|
|
470
586
|
console.log("");
|
|
471
587
|
console.log(t("cli.status.decisions"));
|
|
472
588
|
if ((control.decisionsPending || []).length) {
|
|
473
589
|
control.decisionsPending.forEach((d) => console.log(`- ${d.title} (${d.owner}) — ${d.impact}`));
|
|
474
590
|
} else {
|
|
475
591
|
console.log(`- ${t("cli.status.noDecisions")}`);
|
|
476
|
-
}
|
|
477
|
-
console.log("");
|
|
478
|
-
console.log(t("cli.status.repo"));
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.log(`- ${t("cli.status.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
592
|
+
}
|
|
593
|
+
console.log("");
|
|
594
|
+
console.log(t("cli.status.repo"));
|
|
595
|
+
if (!repo.available) {
|
|
596
|
+
console.log(`- ${t("cli.status.gitState", { state: t("cli.status.gitNotInitialized") })}`);
|
|
597
|
+
console.log(`- ${t("cli.status.gitAction")}`);
|
|
598
|
+
} else {
|
|
599
|
+
const branchLabel = repo.state === "detached" ? t("cli.status.branchDetached") : repo.branch;
|
|
600
|
+
const treeStatus = repo.clean
|
|
601
|
+
? t("cli.status.treeClean")
|
|
602
|
+
: t("cli.status.treeDirty", { staged: repo.staged, unstaged: repo.unstaged, untracked: repo.untracked });
|
|
603
|
+
console.log(`- ${t("cli.status.branch", { branch: branchLabel, treeStatus })}`);
|
|
604
|
+
if (repo.lastCommit) {
|
|
605
|
+
console.log(`- ${t("cli.status.lastCommit", { hash: repo.lastCommit.shortHash, subject: repo.lastCommit.subject })}`);
|
|
606
|
+
}
|
|
607
|
+
if (!repo.hasUpstream) {
|
|
608
|
+
console.log(`- ${t("cli.status.noUpstream")}`);
|
|
609
|
+
} else if (repo.ahead || repo.behind) {
|
|
610
|
+
console.log(`- ${t("cli.status.divergence", { ahead: repo.ahead, behind: repo.behind })}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
console.log(`- ${t("cli.status.runtime", { path: path.relative(context.workspaceRoot, config.runtimeFilePath(context)) })}`);
|
|
614
|
+
console.log(`- ${t("cli.status.envPresent", { value: listOrNone(envAudit.presentKeys) })}`);
|
|
615
|
+
console.log(`- ${t("cli.status.envMissing", { value: listOrNone(envAudit.missingKeys) })}`);
|
|
616
|
+
console.log("");
|
|
617
|
+
const syncStatus = drift.length
|
|
618
|
+
? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
|
|
495
619
|
: t("cli.status.docsSyncedYes");
|
|
496
620
|
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
497
621
|
}
|
|
@@ -500,9 +624,22 @@ function cmdNext(root) {
|
|
|
500
624
|
const context = config.ensureContext(root);
|
|
501
625
|
initLocale(context);
|
|
502
626
|
const control = config.loadControl(context);
|
|
503
|
-
const
|
|
627
|
+
const state = derive(control);
|
|
628
|
+
if (state.circularDeps.length) {
|
|
629
|
+
console.log(t("control.circularDependency", { taskIds: state.circularDeps.join(", ") }));
|
|
630
|
+
}
|
|
631
|
+
const ready = state.readyTasks.slice(0, 10);
|
|
504
632
|
if (!ready.length) {
|
|
505
|
-
|
|
633
|
+
if (state.projectCompleted) {
|
|
634
|
+
console.log(t("cli.noReadyTasks.allDone"));
|
|
635
|
+
} else if (state.blockers.length) {
|
|
636
|
+
console.log(t("cli.noReadyTasks.blocked", { count: state.blockers.length }));
|
|
637
|
+
for (const task of state.blockers.slice(0, 5)) {
|
|
638
|
+
console.log(` - ${task.id}: ${task.blocker || task.title}`);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
console.log(t("cli.noReadyTasks"));
|
|
642
|
+
}
|
|
506
643
|
return;
|
|
507
644
|
}
|
|
508
645
|
ready.forEach((task, i) => {
|
|
@@ -513,12 +650,34 @@ function cmdNext(root) {
|
|
|
513
650
|
});
|
|
514
651
|
}
|
|
515
652
|
|
|
516
|
-
function cmdSync(root) {
|
|
653
|
+
function cmdSync(root, args) {
|
|
517
654
|
const context = config.ensureContext(root);
|
|
518
655
|
initLocale(context);
|
|
519
656
|
const control = config.loadControl(context);
|
|
657
|
+
if ((args || []).includes("--dry-run")) {
|
|
658
|
+
const drift = getDocDrift(context, control);
|
|
659
|
+
if (drift.length) {
|
|
660
|
+
console.log(t("cli.sync.dryRunWouldUpdate", { files: drift.join(", ") }));
|
|
661
|
+
} else {
|
|
662
|
+
console.log(t("cli.sync.dryRunInSync"));
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const state = derive(control);
|
|
667
|
+
if (state.activePhase && control.meta.focusPhase !== state.activePhase.id) {
|
|
668
|
+
control.meta.focusPhase = state.activePhase.id;
|
|
669
|
+
config.saveControl(context, control);
|
|
670
|
+
}
|
|
520
671
|
env.syncEnvironment(context, control);
|
|
521
|
-
|
|
672
|
+
if (config.isOperaInstalled(control)) {
|
|
673
|
+
const bootstrap = require("./opera-bootstrap");
|
|
674
|
+
const result = bootstrap.revalidateContract(context, control);
|
|
675
|
+
if (result.changed) {
|
|
676
|
+
console.log(t("control.contractStale"));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const force = (args || []).includes("--force");
|
|
680
|
+
syncDocs(context, control, { force });
|
|
522
681
|
refreshRepoRuntime(context, { quiet: true });
|
|
523
682
|
console.log(t("cli.docsSynced"));
|
|
524
683
|
}
|
|
@@ -593,11 +752,14 @@ function cmdHelp() {
|
|
|
593
752
|
console.log(" skill install|list|remove|catalog <name>");
|
|
594
753
|
console.log(` ${t("cli.help.skill.desc")}`);
|
|
595
754
|
console.log(" help");
|
|
596
|
-
console.log(` ${t("cli.help.help.desc")}`);
|
|
597
|
-
console.log("");
|
|
598
|
-
console.log(t("cli.help.
|
|
599
|
-
console.log(` ${t("cli.help.
|
|
600
|
-
console.log(
|
|
755
|
+
console.log(` ${t("cli.help.help.desc")}`);
|
|
756
|
+
console.log("");
|
|
757
|
+
console.log(t("cli.help.globalFlags"));
|
|
758
|
+
console.log(` ${t("cli.help.globalFlags.line1")}`);
|
|
759
|
+
console.log("");
|
|
760
|
+
console.log(t("cli.help.globalWorkflow"));
|
|
761
|
+
console.log(` ${t("cli.help.globalWorkflow.line1")}`);
|
|
762
|
+
console.log(` ${t("cli.help.globalWorkflow.line2")}`);
|
|
601
763
|
}
|
|
602
764
|
|
|
603
765
|
/* ── project-scoped API (used by server) ── */
|
package/lib/env.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const config = require("./config");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const config = require("./config");
|
|
6
|
+
const { t, setLocale } = require("./i18n");
|
|
6
7
|
|
|
7
8
|
const SERVICE_ENV_KEYS = {
|
|
8
9
|
OpenAI: ["OPENAI_API_KEY"],
|
|
@@ -184,7 +185,7 @@ function syncEnvironment(contextOrRoot, controlState, options = {}) {
|
|
|
184
185
|
return auditEnvironment(context, control);
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
function auditEnvironment(contextOrRoot, controlState) {
|
|
188
|
+
function auditEnvironment(contextOrRoot, controlState) {
|
|
188
189
|
const context = config.ensureContext(contextOrRoot);
|
|
189
190
|
const control = controlState || config.loadControl(context);
|
|
190
191
|
const envMeta = normalizeEnvironmentMeta(control, context);
|
|
@@ -206,30 +207,41 @@ function auditEnvironment(contextOrRoot, controlState) {
|
|
|
206
207
|
presentKeys,
|
|
207
208
|
missingKeys,
|
|
208
209
|
lastAuditAt: envMeta.lastAuditAt,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.log(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function initLocale(contextOrRoot) {
|
|
214
|
+
try {
|
|
215
|
+
const control = config.loadControl(config.ensureContext(contextOrRoot));
|
|
216
|
+
setLocale(config.getLocale(control));
|
|
217
|
+
} catch (_error) {
|
|
218
|
+
setLocale("es");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cmdStatus(contextOrRoot) {
|
|
223
|
+
initLocale(contextOrRoot);
|
|
224
|
+
const audit = auditEnvironment(contextOrRoot);
|
|
225
|
+
console.log(t("env.status.title"));
|
|
226
|
+
console.log(t("env.status.rootEnv", { path: audit.files.rootEnv }));
|
|
227
|
+
console.log(t("env.status.example", { path: audit.files.rootExample }));
|
|
228
|
+
console.log(t("env.status.appBridge", { path: audit.files.appBridge }));
|
|
229
|
+
console.log(t("env.status.bridgeMode", { value: audit.bridgeMode }));
|
|
230
|
+
console.log(t("env.status.required", { value: audit.requiredKeys.length ? audit.requiredKeys.join(", ") : t("locale.none") }));
|
|
231
|
+
console.log(t("env.status.present", { value: audit.presentKeys.length ? audit.presentKeys.join(", ") : t("locale.none") }));
|
|
232
|
+
console.log(t("env.status.missing", { value: audit.missingKeys.length ? audit.missingKeys.join(", ") : t("locale.none") }));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cmdSync(contextOrRoot) {
|
|
236
|
+
initLocale(contextOrRoot);
|
|
237
|
+
const context = config.ensureContext(contextOrRoot);
|
|
238
|
+
const control = config.loadControl(context);
|
|
239
|
+
const audit = syncEnvironment(context, control);
|
|
240
|
+
console.log(t("env.sync.updated", { path: path.relative(context.workspaceRoot, context.env.rootFile) }));
|
|
241
|
+
if (audit.missingKeys.length) {
|
|
242
|
+
console.log(t("env.sync.missing", { value: audit.missingKeys.join(", ") }));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
233
245
|
|
|
234
246
|
module.exports = {
|
|
235
247
|
SERVICE_ENV_KEYS,
|
package/lib/i18n.js
CHANGED
|
@@ -38,10 +38,11 @@ function getLocale() {
|
|
|
38
38
|
|
|
39
39
|
function t(key, params) {
|
|
40
40
|
ensureLoaded();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
key;
|
|
41
|
+
const found = currentMessages[key] || (fallbackMessages && fallbackMessages[key]);
|
|
42
|
+
let message = found || key;
|
|
43
|
+
if (!found && process.env.NODE_ENV !== "production" && process.env.TRACKOPS_DEBUG) {
|
|
44
|
+
process.stderr.write(`[i18n] Missing key: ${key}\n`);
|
|
45
|
+
}
|
|
45
46
|
if (params) {
|
|
46
47
|
message = message.replace(/\{(\w+)\}/g, (match, paramKey) =>
|
|
47
48
|
params[paramKey] !== undefined ? String(params[paramKey]) : match
|