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/lib/control.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { spawnSync } = require("child_process");
|
|
6
|
-
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawnSync } = require("child_process");
|
|
6
|
+
|
|
7
7
|
const config = require("./config");
|
|
8
8
|
const env = require("./env");
|
|
9
9
|
const { t, setLocale, getLocale } = require("./i18n");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
+
|
|
54
83
|
function getRepoSnapshot(contextOrRoot) {
|
|
55
84
|
const context = config.ensureContext(contextOrRoot);
|
|
56
85
|
const repoRoot = context.workspaceRoot;
|
|
57
|
-
const
|
|
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() : "";
|
|
58
106
|
const status = git(["status", "--short"], repoRoot) || "";
|
|
59
107
|
const lines = status.split(/\r?\n/).filter(Boolean);
|
|
60
108
|
const lastCommitRaw = git(["log", "-1", "--pretty=format:%H%n%cs%n%s"], repoRoot);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,27 +129,40 @@ 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 { generatedAt: nowIso(), branch, clean: lines.length === 0, staged, unstaged, untracked, ahead, behind, lastCommit };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function refreshRepoRuntime(root, options = {}) {
|
|
91
|
-
const context = config.ensureContext(root);
|
|
92
|
-
const runtimeFile = config.runtimeFilePath(context);
|
|
93
|
-
fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });
|
|
94
|
-
const snapshot = getRepoSnapshot(context);
|
|
95
|
-
writeJson(runtimeFile, snapshot);
|
|
96
|
-
if (!options.quiet) {
|
|
97
|
-
console.log(t("cli.runtimeUpdated", { path: path.relative(context.workspaceRoot, runtimeFile) }));
|
|
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;
|
|
98
136
|
}
|
|
99
|
-
|
|
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
|
+
};
|
|
100
152
|
}
|
|
101
153
|
|
|
154
|
+
function refreshRepoRuntime(root, options = {}) {
|
|
155
|
+
const context = config.ensureContext(root);
|
|
156
|
+
const runtimeFile = config.runtimeFilePath(context);
|
|
157
|
+
fs.mkdirSync(path.dirname(runtimeFile), { recursive: true });
|
|
158
|
+
const snapshot = getRepoSnapshot(context);
|
|
159
|
+
writeJson(runtimeFile, snapshot);
|
|
160
|
+
if (!options.quiet) {
|
|
161
|
+
console.log(t("cli.runtimeUpdated", { path: path.relative(context.workspaceRoot, runtimeFile) }));
|
|
162
|
+
}
|
|
163
|
+
return snapshot;
|
|
164
|
+
}
|
|
165
|
+
|
|
102
166
|
/* ── derive ── */
|
|
103
167
|
|
|
104
168
|
function getPhaseInfo(phaseId, phases) {
|
|
@@ -115,16 +179,42 @@ function compareTasks(a, b, phases) {
|
|
|
115
179
|
return a.title.localeCompare(b.title, getLocale());
|
|
116
180
|
}
|
|
117
181
|
|
|
118
|
-
function
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
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
|
+
|
|
199
|
+
function derive(control) {
|
|
200
|
+
const phases = config.getPhases(control);
|
|
201
|
+
const tasks = [...control.tasks].sort((a, b) => compareTasks(a, b, phases));
|
|
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));
|
|
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,22 +229,27 @@ 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
|
-
const phaseStats = phases.map((phase) => {
|
|
148
|
-
const phaseTasks = tasks.filter((t) => t.phase === phase.id && t.required !== false);
|
|
149
|
-
const completed = phaseTasks.filter((t) => t.status === "completed").length;
|
|
150
|
-
const closed = phaseTasks.filter((t) => closedStatuses.has(t.status)).length;
|
|
151
|
-
return { ...phase, total: phaseTasks.length, completed, closed, remaining: phaseTasks.length - closed };
|
|
152
|
-
});
|
|
238
|
+
const phaseStats = phases.map((phase) => {
|
|
239
|
+
const phaseTasks = tasks.filter((t) => t.phase === phase.id && t.required !== false);
|
|
240
|
+
const completed = phaseTasks.filter((t) => t.status === "completed").length;
|
|
241
|
+
const closed = phaseTasks.filter((t) => closedStatuses.has(t.status)).length;
|
|
242
|
+
return { ...phase, total: phaseTasks.length, completed, closed, remaining: phaseTasks.length - closed };
|
|
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
|
"",
|
|
@@ -359,11 +455,11 @@ function buildDocMap(control) {
|
|
|
359
455
|
return { taskPlan: renderTaskPlan(control), progress: renderProgress(control), findings: renderFindings(control) };
|
|
360
456
|
}
|
|
361
457
|
|
|
362
|
-
function getDocDrift(root, control) {
|
|
363
|
-
const context = config.ensureContext(root);
|
|
364
|
-
const docs = buildDocMap(control);
|
|
365
|
-
const docFiles = config.docFilePaths(context);
|
|
366
|
-
return Object.entries({ task_plan: [docFiles.taskPlan, docs.taskPlan], progress: [docFiles.progress, docs.progress], findings: [docFiles.findings, docs.findings] })
|
|
458
|
+
function getDocDrift(root, control) {
|
|
459
|
+
const context = config.ensureContext(root);
|
|
460
|
+
const docs = buildDocMap(control);
|
|
461
|
+
const docFiles = config.docFilePaths(context);
|
|
462
|
+
return Object.entries({ task_plan: [docFiles.taskPlan, docs.taskPlan], progress: [docFiles.progress, docs.progress], findings: [docFiles.findings, docs.findings] })
|
|
367
463
|
.filter(([, [filePath, expected]]) => {
|
|
368
464
|
if (!fs.existsSync(filePath)) return true;
|
|
369
465
|
return fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n") !== `${expected}\n`;
|
|
@@ -371,21 +467,34 @@ function getDocDrift(root, control) {
|
|
|
371
467
|
.map(([name]) => name);
|
|
372
468
|
}
|
|
373
469
|
|
|
374
|
-
function syncDocs(root, control) {
|
|
375
|
-
const context = config.ensureContext(root);
|
|
376
|
-
const docs = buildDocMap(control);
|
|
377
|
-
const docFiles = config.docFilePaths(context);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
470
|
+
function syncDocs(root, control, options = {}) {
|
|
471
|
+
const context = config.ensureContext(root);
|
|
472
|
+
const docs = buildDocMap(control);
|
|
473
|
+
const docFiles = config.docFilePaths(context);
|
|
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
|
+
}
|
|
490
|
+
}
|
|
382
491
|
|
|
383
492
|
/* ── task management ── */
|
|
384
493
|
|
|
385
|
-
function updateTask(root, control, action, taskId, note) {
|
|
386
|
-
const context = config.ensureContext(root);
|
|
387
|
-
const task = control.tasks.find((item) => item.id === taskId);
|
|
388
|
-
if (!task) throw new Error(t("cli.taskNotFound", { taskId }));
|
|
494
|
+
function updateTask(root, control, action, taskId, note) {
|
|
495
|
+
const context = config.ensureContext(root);
|
|
496
|
+
const task = control.tasks.find((item) => item.id === taskId);
|
|
497
|
+
if (!task) throw new Error(t("cli.taskNotFound", { taskId }));
|
|
389
498
|
|
|
390
499
|
const actionMap = { start: "in_progress", review: "in_review", complete: "completed", done: "completed", block: "blocked", pending: "pending", cancel: "cancelled" };
|
|
391
500
|
|
|
@@ -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");
|
|
@@ -406,47 +519,50 @@ function updateTask(root, control, action, taskId, note) {
|
|
|
406
519
|
task.history.push({ at: nowIso(), action, note: note || "" });
|
|
407
520
|
}
|
|
408
521
|
|
|
409
|
-
config.saveControl(context, control);
|
|
410
|
-
syncDocs(context, control);
|
|
411
|
-
refreshRepoRuntime(context, { quiet: true });
|
|
412
|
-
}
|
|
522
|
+
config.saveControl(context, control);
|
|
523
|
+
syncDocs(context, control);
|
|
524
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
525
|
+
}
|
|
413
526
|
|
|
414
527
|
/* ── CLI commands ── */
|
|
415
528
|
|
|
416
|
-
function initLocale(root) {
|
|
417
|
-
try {
|
|
418
|
-
const control = config.loadControl(config.ensureContext(root));
|
|
419
|
-
setLocale(config.getLocale(control));
|
|
420
|
-
} catch (_err) {
|
|
421
|
-
setLocale("es");
|
|
529
|
+
function initLocale(root) {
|
|
530
|
+
try {
|
|
531
|
+
const control = config.loadControl(config.ensureContext(root));
|
|
532
|
+
setLocale(config.getLocale(control));
|
|
533
|
+
} catch (_err) {
|
|
534
|
+
setLocale("es");
|
|
422
535
|
}
|
|
423
536
|
}
|
|
424
537
|
|
|
425
|
-
function cmdStatus(root) {
|
|
426
|
-
const context = config.ensureContext(root);
|
|
427
|
-
initLocale(context);
|
|
428
|
-
const control = config.loadControl(context);
|
|
429
|
-
const state = derive(control);
|
|
430
|
-
const phases = config.getPhases(control);
|
|
431
|
-
const repo = refreshRepoRuntime(context, { quiet: true });
|
|
432
|
-
const drift = getDocDrift(context, control);
|
|
433
|
-
const envAudit = env.auditEnvironment(context, control);
|
|
434
|
-
|
|
538
|
+
function cmdStatus(root) {
|
|
539
|
+
const context = config.ensureContext(root);
|
|
540
|
+
initLocale(context);
|
|
541
|
+
const control = config.loadControl(context);
|
|
542
|
+
const state = derive(control);
|
|
543
|
+
const phases = config.getPhases(control);
|
|
544
|
+
const repo = refreshRepoRuntime(context, { quiet: true });
|
|
545
|
+
const drift = getDocDrift(context, control);
|
|
546
|
+
const envAudit = env.auditEnvironment(context, control);
|
|
547
|
+
|
|
435
548
|
console.log(t("cli.status.title", { projectName: control.meta.projectName }));
|
|
436
549
|
console.log(t("cli.status.focus", { focus: control.meta.currentFocus }));
|
|
437
550
|
console.log(t("cli.status.activePhase", { phaseId: state.activePhase.id, phaseLabel: state.activePhase.label }));
|
|
438
|
-
console.log(
|
|
551
|
+
console.log(t("cli.status.layout", { layout: context.layout, workspace: context.workspaceRoot }));
|
|
439
552
|
if (context.layout === "split") {
|
|
440
|
-
console.log(
|
|
441
|
-
console.log(
|
|
553
|
+
console.log(t("cli.status.appRoot", { path: context.appRoot }));
|
|
554
|
+
console.log(t("cli.status.opsRoot", { path: context.opsRoot }));
|
|
442
555
|
}
|
|
443
556
|
if (control.meta?.opera?.bootstrap?.status) {
|
|
444
|
-
console.log(t("cli.status.bootstrap", {
|
|
557
|
+
console.log(t("cli.status.bootstrap", {
|
|
558
|
+
status: formatBootstrapStatus(control.meta.opera.bootstrap.status),
|
|
559
|
+
locale: config.getLocale(control),
|
|
560
|
+
}));
|
|
445
561
|
if (control.meta.opera.bootstrap.mode) {
|
|
446
|
-
console.log(
|
|
562
|
+
console.log(t("cli.status.bootstrapMode", { value: formatBootstrapMode(control.meta.opera.bootstrap.mode) }));
|
|
447
563
|
}
|
|
448
564
|
if (control.meta.opera.bootstrap.routeReason) {
|
|
449
|
-
console.log(
|
|
565
|
+
console.log(t("cli.status.bootstrapReason", { value: formatBootstrapReason(control.meta.opera.bootstrap.routeReason) }));
|
|
450
566
|
}
|
|
451
567
|
}
|
|
452
568
|
console.log(t("cli.status.tasks", {
|
|
@@ -454,41 +570,49 @@ function cmdStatus(root) {
|
|
|
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.
|
|
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
|
+
}
|
|
488
612
|
}
|
|
489
613
|
console.log(`- ${t("cli.status.runtime", { path: path.relative(context.workspaceRoot, config.runtimeFilePath(context)) })}`);
|
|
490
|
-
console.log(`-
|
|
491
|
-
console.log(`-
|
|
614
|
+
console.log(`- ${t("cli.status.envPresent", { value: listOrNone(envAudit.presentKeys) })}`);
|
|
615
|
+
console.log(`- ${t("cli.status.envMissing", { value: listOrNone(envAudit.missingKeys) })}`);
|
|
492
616
|
console.log("");
|
|
493
617
|
const syncStatus = drift.length
|
|
494
618
|
? t("cli.status.docsSyncedNo", { files: drift.join(", ") })
|
|
@@ -496,13 +620,26 @@ function cmdStatus(root) {
|
|
|
496
620
|
console.log(t("cli.status.docsSynced", { status: syncStatus }));
|
|
497
621
|
}
|
|
498
622
|
|
|
499
|
-
function cmdNext(root) {
|
|
500
|
-
const context = config.ensureContext(root);
|
|
501
|
-
initLocale(context);
|
|
502
|
-
const control = config.loadControl(context);
|
|
503
|
-
const
|
|
623
|
+
function cmdNext(root) {
|
|
624
|
+
const context = config.ensureContext(root);
|
|
625
|
+
initLocale(context);
|
|
626
|
+
const control = config.loadControl(context);
|
|
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,114 +650,139 @@ function cmdNext(root) {
|
|
|
513
650
|
});
|
|
514
651
|
}
|
|
515
652
|
|
|
516
|
-
function cmdSync(root) {
|
|
517
|
-
const context = config.ensureContext(root);
|
|
518
|
-
initLocale(context);
|
|
519
|
-
const control = config.loadControl(context);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
console.log(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
console.log("
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
console.log("
|
|
573
|
-
console.log(
|
|
574
|
-
console.log("
|
|
575
|
-
console.log(
|
|
576
|
-
console.log("
|
|
577
|
-
console.log(
|
|
578
|
-
console.log("
|
|
579
|
-
console.log(
|
|
580
|
-
console.log("
|
|
581
|
-
console.log(` ${t("cli.help.
|
|
582
|
-
console.log("
|
|
583
|
-
console.log(` ${t("cli.help.
|
|
584
|
-
console.log("
|
|
585
|
-
console.log(` ${t("cli.help.
|
|
586
|
-
console.log("
|
|
587
|
-
console.log(` ${t("cli.help.
|
|
588
|
-
console.log(
|
|
589
|
-
console.log("
|
|
590
|
-
console.log(
|
|
591
|
-
console.log("
|
|
592
|
-
console.log(
|
|
593
|
-
console.log("
|
|
594
|
-
console.log(
|
|
595
|
-
console.log("
|
|
653
|
+
function cmdSync(root, args) {
|
|
654
|
+
const context = config.ensureContext(root);
|
|
655
|
+
initLocale(context);
|
|
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
|
+
}
|
|
671
|
+
env.syncEnvironment(context, control);
|
|
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 });
|
|
681
|
+
refreshRepoRuntime(context, { quiet: true });
|
|
682
|
+
console.log(t("cli.docsSynced"));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function cmdRefreshRepo(root, args) {
|
|
686
|
+
refreshRepoRuntime(config.ensureContext(root), { quiet: (args || []).includes("--quiet") });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function cmdTask(root, args) {
|
|
690
|
+
const context = config.ensureContext(root);
|
|
691
|
+
initLocale(context);
|
|
692
|
+
const [action, taskId, ...noteParts] = args || [];
|
|
693
|
+
if (!action || !taskId) throw new Error(t("cli.mustProvideActionAndId"));
|
|
694
|
+
const control = config.loadControl(context);
|
|
695
|
+
updateTask(context, control, action, taskId, noteParts.join(" ").trim());
|
|
696
|
+
console.log(t("cli.taskUpdated", { taskId, action }));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function cmdInstallHooks(root) {
|
|
700
|
+
const context = config.ensureContext(root);
|
|
701
|
+
initLocale(context);
|
|
702
|
+
const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
|
|
703
|
+
const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
|
|
704
|
+
if (result.error || result.status !== 0) throw new Error(t("cli.hooksError"));
|
|
705
|
+
console.log(t("cli.hooksInstalled"));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function cmdHelp() {
|
|
709
|
+
console.log(`trackops — ${t("cli.help.title")}`);
|
|
710
|
+
console.log("");
|
|
711
|
+
console.log(`${t("cli.help.usage")} trackops <command> [args]`);
|
|
712
|
+
console.log("");
|
|
713
|
+
console.log(t("cli.help.commands"));
|
|
714
|
+
console.log(" init [--with-opera] [--legacy-layout] [--locale es|en] [--name \"...\"] [--no-bootstrap]");
|
|
715
|
+
console.log(" [--bootstrap-mode auto|direct|handoff] [--technical-level low|medium|high|senior]");
|
|
716
|
+
console.log(" [--project-state idea|draft|existing_repo|advanced] [--docs-state none|notes|sos|spec_dossier|repo_docs]");
|
|
717
|
+
console.log(" [--decision-ownership user|shared|agent]");
|
|
718
|
+
console.log(` ${t("cli.help.init.desc")}`);
|
|
719
|
+
console.log(" workspace status|migrate");
|
|
720
|
+
console.log(` ${t("cli.help.workspace.desc")}`);
|
|
721
|
+
console.log(" env status|sync");
|
|
722
|
+
console.log(` ${t("cli.help.env.desc")}`);
|
|
723
|
+
console.log(" release [--push]");
|
|
724
|
+
console.log(` ${t("cli.help.release.desc")}`);
|
|
725
|
+
console.log(" version");
|
|
726
|
+
console.log(` ${t("cli.help.version.desc")}`);
|
|
727
|
+
console.log(" status");
|
|
728
|
+
console.log(` ${t("cli.help.status.desc")}`);
|
|
729
|
+
console.log(" next");
|
|
730
|
+
console.log(` ${t("cli.help.next.desc")}`);
|
|
731
|
+
console.log(" sync");
|
|
732
|
+
console.log(` ${t("cli.help.sync.desc")}`);
|
|
733
|
+
console.log(" dashboard [--port N] [--host HOST] [--public] [--strict-port]");
|
|
734
|
+
console.log(` ${t("cli.help.dashboard.desc")}`);
|
|
735
|
+
console.log(" refresh-repo [--quiet]");
|
|
736
|
+
console.log(` ${t("cli.help.refreshRepo.desc")}`);
|
|
737
|
+
console.log(" install-hooks");
|
|
738
|
+
console.log(` ${t("cli.help.installHooks.desc")}`);
|
|
739
|
+
console.log(" register");
|
|
740
|
+
console.log(` ${t("cli.help.register.desc")}`);
|
|
741
|
+
console.log(" projects");
|
|
742
|
+
console.log(` ${t("cli.help.projects.desc")}`);
|
|
743
|
+
console.log(" task <action> <id> [note]");
|
|
744
|
+
console.log(` ${t("cli.help.task.desc")}`);
|
|
745
|
+
console.log(" opera install|bootstrap|handoff|status|configure|upgrade");
|
|
746
|
+
console.log(` ${t("cli.help.opera.desc")}`);
|
|
747
|
+
console.log(` ${t("cli.help.opera.upgradeHint")}`);
|
|
748
|
+
console.log(" locale get|set [es|en]");
|
|
749
|
+
console.log(` ${t("cli.help.locale.desc")}`);
|
|
750
|
+
console.log(" doctor locale");
|
|
751
|
+
console.log(` ${t("cli.help.doctor.desc")}`);
|
|
752
|
+
console.log(" skill install|list|remove|catalog <name>");
|
|
753
|
+
console.log(` ${t("cli.help.skill.desc")}`);
|
|
754
|
+
console.log(" help");
|
|
596
755
|
console.log(` ${t("cli.help.help.desc")}`);
|
|
597
756
|
console.log("");
|
|
757
|
+
console.log(t("cli.help.globalFlags"));
|
|
758
|
+
console.log(` ${t("cli.help.globalFlags.line1")}`);
|
|
759
|
+
console.log("");
|
|
598
760
|
console.log(t("cli.help.globalWorkflow"));
|
|
599
761
|
console.log(` ${t("cli.help.globalWorkflow.line1")}`);
|
|
600
762
|
console.log(` ${t("cli.help.globalWorkflow.line2")}`);
|
|
601
|
-
}
|
|
763
|
+
}
|
|
602
764
|
|
|
603
765
|
/* ── project-scoped API (used by server) ── */
|
|
604
766
|
|
|
605
|
-
function forProject(root) {
|
|
606
|
-
const context = config.ensureContext(root);
|
|
607
|
-
initLocale(context);
|
|
608
|
-
return {
|
|
609
|
-
loadControl: () => config.loadControl(context),
|
|
610
|
-
saveControl: (ctrl) => config.saveControl(context, ctrl),
|
|
611
|
-
derive,
|
|
612
|
-
buildDocMap,
|
|
613
|
-
getDocDrift: (ctrl) => getDocDrift(context, ctrl),
|
|
614
|
-
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
615
|
-
updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
|
|
616
|
-
getRepoSnapshot: () => getRepoSnapshot(context),
|
|
617
|
-
refreshRepoRuntime: (opts) => refreshRepoRuntime(context, opts),
|
|
618
|
-
getPhases: (ctrl) => config.getPhases(ctrl),
|
|
619
|
-
getLocale: (ctrl) => config.getLocale(ctrl),
|
|
620
|
-
statusLabel,
|
|
621
|
-
context,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
767
|
+
function forProject(root) {
|
|
768
|
+
const context = config.ensureContext(root);
|
|
769
|
+
initLocale(context);
|
|
770
|
+
return {
|
|
771
|
+
loadControl: () => config.loadControl(context),
|
|
772
|
+
saveControl: (ctrl) => config.saveControl(context, ctrl),
|
|
773
|
+
derive,
|
|
774
|
+
buildDocMap,
|
|
775
|
+
getDocDrift: (ctrl) => getDocDrift(context, ctrl),
|
|
776
|
+
syncDocs: (ctrl) => syncDocs(context, ctrl),
|
|
777
|
+
updateTask: (ctrl, action, id, note) => updateTask(context, ctrl, action, id, note),
|
|
778
|
+
getRepoSnapshot: () => getRepoSnapshot(context),
|
|
779
|
+
refreshRepoRuntime: (opts) => refreshRepoRuntime(context, opts),
|
|
780
|
+
getPhases: (ctrl) => config.getPhases(ctrl),
|
|
781
|
+
getLocale: (ctrl) => config.getLocale(ctrl),
|
|
782
|
+
statusLabel,
|
|
783
|
+
context,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
624
786
|
|
|
625
787
|
module.exports = {
|
|
626
788
|
buildDocMap, derive, getDocDrift, getRepoSnapshot, refreshRepoRuntime, syncDocs, updateTask,
|