trackops 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +907 -554
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +7 -9
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/lib/opera.js
CHANGED
|
@@ -1,202 +1,351 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
6
|
const config = require("./config");
|
|
7
|
+
const env = require("./env");
|
|
7
8
|
const { t, setLocale } = require("./i18n");
|
|
9
|
+
const { promptForLocale, resolveLocale } = require("./locale");
|
|
10
|
+
const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
|
|
11
|
+
const bootstrap = require("./opera-bootstrap");
|
|
8
12
|
|
|
9
13
|
const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
|
|
10
14
|
const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
|
|
11
15
|
const OPERA_VERSION = require("../package.json").version;
|
|
12
|
-
|
|
13
|
-
function nowIso() {
|
|
14
|
-
return new Date().toISOString();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function
|
|
18
|
-
fs.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
16
|
+
|
|
17
|
+
function nowIso() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readText(filePath) {
|
|
22
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderTemplate(templatePath, replacements) {
|
|
26
|
+
let content = fs.readFileSync(templatePath, "utf8");
|
|
27
|
+
for (const [key, value] of Object.entries(replacements || {})) {
|
|
28
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
29
|
+
}
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesKnownTemplate(filePath, templatePath, replacements) {
|
|
34
|
+
if (!fs.existsSync(filePath) || !fs.existsSync(templatePath)) return false;
|
|
35
|
+
return readText(filePath).replace(/\r\n/g, "\n") === renderTemplate(templatePath, replacements).replace(/\r\n/g, "\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyTemplate(templatePath, targetPath, replacements, options = {}) {
|
|
39
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
40
|
+
const shouldWrite =
|
|
41
|
+
!fs.existsSync(targetPath) ||
|
|
42
|
+
options.overwrite === true ||
|
|
43
|
+
(options.overwriteIfTemplate && matchesKnownTemplate(targetPath, templatePath, replacements));
|
|
44
|
+
|
|
45
|
+
if (!shouldWrite) return false;
|
|
46
|
+
fs.writeFileSync(targetPath, renderTemplate(templatePath, replacements), "utf8");
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function copyDirRecursive(src, dest) {
|
|
51
|
+
if (!src || !fs.existsSync(src)) return;
|
|
52
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
53
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
54
|
+
const srcPath = path.join(src, entry.name);
|
|
55
|
+
const destPath = path.join(dest, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
copyDirRecursive(srcPath, destPath);
|
|
58
|
+
} else {
|
|
59
|
+
fs.copyFileSync(srcPath, destPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveOperaLocale(control, options = {}) {
|
|
65
|
+
return resolveLocale(options.locale, config.getLocale(control));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installStructure(root, control, locale, options = {}) {
|
|
69
|
+
const context = config.ensureContext(root);
|
|
55
70
|
const projectName = control.meta.projectName || "Project";
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
const replacements = {
|
|
72
|
+
PROJECT_NAME: projectName,
|
|
73
|
+
DESIRED_OUTCOME: t("bootstrap.pendingValue"),
|
|
74
|
+
SERVICES_TABLE: "| — | — | — |",
|
|
75
|
+
SOURCE_OF_TRUTH: t("bootstrap.pendingValue"),
|
|
76
|
+
PAYLOAD: t("bootstrap.pendingValue"),
|
|
77
|
+
BEHAVIOR_RULES: `- ${t("bootstrap.noneDefined")}`,
|
|
78
|
+
DATA_SCHEMA: JSON.stringify({
|
|
79
|
+
input: { source: "", schema: {} },
|
|
80
|
+
output: { destination: "", schema: {} },
|
|
81
|
+
}, null, 2),
|
|
82
|
+
ARCHITECTURAL_INVARIANTS: `- ${t("bootstrap.noneDefined")}`,
|
|
83
|
+
PIPELINE_ITEMS: `- ${t("bootstrap.noneDefined")}`,
|
|
84
|
+
TEMPLATE_ITEMS: `- ${t("bootstrap.noneDefined")}`,
|
|
85
|
+
};
|
|
86
|
+
const overwriteOptions = {
|
|
87
|
+
overwriteIfTemplate: options.rewriteLocalizedTemplates === true,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const agentHubDir = context.paths.agentHubDir;
|
|
59
91
|
fs.mkdirSync(agentHubDir, { recursive: true });
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
|
|
93
|
+
copyTemplate(
|
|
94
|
+
resolveLocalizedFile(TEMPLATES_DIR, locale, "agent.md"),
|
|
95
|
+
path.join(agentHubDir, "agent.md"),
|
|
96
|
+
replacements,
|
|
97
|
+
overwriteOptions,
|
|
98
|
+
);
|
|
99
|
+
copyTemplate(
|
|
100
|
+
resolveLocalizedFile(TEMPLATES_DIR, locale, "router.md"),
|
|
101
|
+
path.join(agentHubDir, "router.md"),
|
|
102
|
+
replacements,
|
|
103
|
+
overwriteOptions,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const skillsRegistryDir = context.paths.skillsDir;
|
|
73
107
|
fs.mkdirSync(skillsRegistryDir, { recursive: true });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
copyTemplate(
|
|
109
|
+
resolveLocalizedFile(TEMPLATES_DIR, locale, "registry.md"),
|
|
110
|
+
path.join(skillsRegistryDir, "_registry.md"),
|
|
111
|
+
replacements,
|
|
112
|
+
overwriteOptions,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const genesisTemplatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
|
|
116
|
+
const genesisPath = context.paths.genesisFile;
|
|
117
|
+
if (!fs.existsSync(genesisPath) || bootstrap.isVirginGenesis(readText(genesisPath))) {
|
|
118
|
+
copyTemplate(genesisTemplatePath, genesisPath, replacements, { overwrite: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
|
|
122
|
+
const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
|
|
123
|
+
copyDirRecursive(refsTemplateDir, refsTargetDir);
|
|
124
|
+
|
|
125
|
+
const starterSkillTarget = path.join(context.paths.skillsDir, "project-starter-skill", "SKILL.md");
|
|
126
|
+
const starterSkillTemplate = resolveSkillFile(SKILLS_TEMPLATES_DIR, "project-starter-skill", locale);
|
|
127
|
+
if (starterSkillTemplate) {
|
|
128
|
+
fs.mkdirSync(path.dirname(starterSkillTarget), { recursive: true });
|
|
129
|
+
if (!fs.existsSync(starterSkillTarget) || options.rewriteLocalizedTemplates === true) {
|
|
130
|
+
fs.copyFileSync(starterSkillTemplate, starterSkillTarget);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function install(root, options = {}) {
|
|
136
|
+
const context = config.ensureContext(root);
|
|
137
|
+
const controlFile = config.controlFilePath(context);
|
|
138
|
+
if (!fs.existsSync(controlFile)) {
|
|
139
|
+
throw new Error("project_control.json not found. Run 'trackops init' first.");
|
|
78
140
|
}
|
|
79
141
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
142
|
+
const control = config.loadControl(context);
|
|
143
|
+
let locale = resolveOperaLocale(control, options);
|
|
144
|
+
if (!options.locale && !control.meta?.locale) {
|
|
145
|
+
locale = await promptForLocale(locale);
|
|
146
|
+
}
|
|
147
|
+
control.meta.locale = locale;
|
|
148
|
+
setLocale(locale);
|
|
86
149
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(refsTemplateDir)) {
|
|
90
|
-
const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
|
|
91
|
-
copyDirRecursive(refsTemplateDir, starterSkillDir);
|
|
92
|
-
}
|
|
150
|
+
const alreadyInstalled = config.isOperaInstalled(control);
|
|
151
|
+
installStructure(context, control, locale);
|
|
93
152
|
|
|
94
|
-
// Install project-starter-skill SKILL.md
|
|
95
|
-
const starterSkillTemplate = path.join(SKILLS_TEMPLATES_DIR, "project-starter-skill", "SKILL.md");
|
|
96
|
-
const starterSkillTarget = path.join(root, ".agents", "skills", "project-starter-skill", "SKILL.md");
|
|
97
|
-
if (fs.existsSync(starterSkillTemplate) && !fs.existsSync(starterSkillTarget)) {
|
|
98
|
-
fs.mkdirSync(path.dirname(starterSkillTarget), { recursive: true });
|
|
99
|
-
fs.copyFileSync(starterSkillTemplate, starterSkillTarget);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Mark as installed in control
|
|
103
153
|
control.meta.opera = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
...(control.meta.opera || {}),
|
|
155
|
+
installed: true,
|
|
156
|
+
version: OPERA_VERSION,
|
|
157
|
+
installedAt: control.meta?.opera?.installedAt || nowIso(),
|
|
158
|
+
skills: control.meta?.opera?.skills || [],
|
|
159
|
+
};
|
|
160
|
+
config.saveControl(context, control);
|
|
161
|
+
env.syncEnvironment(context, control);
|
|
162
|
+
|
|
163
|
+
if (!alreadyInstalled) {
|
|
164
|
+
console.log(t("opera.installed", { version: OPERA_VERSION }));
|
|
165
|
+
} else {
|
|
166
|
+
console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
const skills = require("./skills");
|
|
115
170
|
for (const skillName of ["commiter", "changelog-updater"]) {
|
|
116
|
-
try {
|
|
117
|
-
|
|
171
|
+
try {
|
|
172
|
+
skills.installSkill(context, skillName, { locale });
|
|
173
|
+
} catch (_error) {
|
|
174
|
+
// ignore
|
|
175
|
+
}
|
|
118
176
|
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function status(root) {
|
|
122
|
-
const control = config.loadControl(root);
|
|
123
|
-
setLocale(config.getLocale(control));
|
|
124
177
|
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
return;
|
|
178
|
+
if (options.bootstrap !== false) {
|
|
179
|
+
await runBootstrap(context, { locale, answers: options.answers, interactive: options.interactive });
|
|
128
180
|
}
|
|
181
|
+
}
|
|
129
182
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
183
|
+
async function runBootstrap(root, options = {}) {
|
|
184
|
+
const context = config.ensureContext(root);
|
|
185
|
+
const control = config.loadControl(context);
|
|
186
|
+
const locale = resolveOperaLocale(control, options);
|
|
187
|
+
control.meta.locale = locale;
|
|
188
|
+
setLocale(locale);
|
|
189
|
+
|
|
190
|
+
const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
|
|
191
|
+
if (legacyBootstrap && !control.meta.opera?.bootstrap) {
|
|
192
|
+
control.meta.opera = control.meta.opera || {};
|
|
193
|
+
control.meta.opera.bootstrap = legacyBootstrap;
|
|
194
|
+
config.saveControl(context, control);
|
|
195
|
+
}
|
|
134
196
|
|
|
135
|
-
|
|
197
|
+
const profile = await bootstrap.collectBootstrapProfile(context, control, options);
|
|
198
|
+
const updatedControl = bootstrap.applyBootstrap(context, control, profile);
|
|
199
|
+
env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
|
|
200
|
+
const ops = require("./control");
|
|
201
|
+
ops.syncDocs(context, updatedControl);
|
|
202
|
+
ops.refreshRepoRuntime(context, { quiet: true });
|
|
203
|
+
|
|
204
|
+
console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
|
|
205
|
+
return profile;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function status(root) {
|
|
209
|
+
const context = config.ensureContext(root);
|
|
210
|
+
const control = config.loadControl(context);
|
|
211
|
+
setLocale(config.getLocale(control));
|
|
212
|
+
|
|
213
|
+
if (!config.isOperaInstalled(control)) {
|
|
214
|
+
console.log(t("opera.notInstalled"));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const opera = control.meta.opera;
|
|
219
|
+
const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
|
|
220
|
+
console.log(`OPERA v${opera.version}`);
|
|
221
|
+
console.log(` Installed: ${opera.installedAt}`);
|
|
222
|
+
console.log(` Skills: ${(opera.skills || []).join(", ") || "none"}`);
|
|
223
|
+
console.log(` Locale: ${config.getLocale(control)}`);
|
|
224
|
+
|
|
225
|
+
if (bootstrapState) {
|
|
226
|
+
console.log(` Bootstrap: ${bootstrapState.status}`);
|
|
227
|
+
if ((bootstrapState.missingFields || []).length) {
|
|
228
|
+
console.log(` Missing: ${bootstrapState.missingFields.join(", ")}`);
|
|
229
|
+
console.log(" Resume: trackops opera bootstrap --resume");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
136
233
|
const checks = [
|
|
137
|
-
[".agent/hub/agent.md", fs.existsSync(path.join(
|
|
138
|
-
[".agent/hub/router.md", fs.existsSync(path.join(
|
|
139
|
-
[".agents/skills/_registry.md", fs.existsSync(
|
|
140
|
-
["genesis.md", fs.existsSync(
|
|
234
|
+
[".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
|
|
235
|
+
[".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
|
|
236
|
+
[".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
|
|
237
|
+
["genesis.md", fs.existsSync(context.paths.genesisFile)],
|
|
141
238
|
];
|
|
142
|
-
|
|
143
|
-
console.log(" Structure:");
|
|
144
|
-
for (const [file, exists] of checks) {
|
|
145
|
-
console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
239
|
+
|
|
240
|
+
console.log(" Structure:");
|
|
241
|
+
for (const [file, exists] of checks) {
|
|
242
|
+
console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
149
246
|
function configure(root, args) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
247
|
+
const context = config.ensureContext(root);
|
|
248
|
+
const control = config.loadControl(context);
|
|
249
|
+
setLocale(config.getLocale(control));
|
|
250
|
+
let nextLocale = config.getLocale(control);
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
253
|
+
if (args[i] === "--locale" && args[i + 1]) {
|
|
254
|
+
nextLocale = resolveLocale(args[i + 1], nextLocale);
|
|
255
|
+
control.meta.locale = nextLocale;
|
|
256
|
+
i += 1;
|
|
257
|
+
}
|
|
258
|
+
if (args[i] === "--phases" && args[i + 1]) {
|
|
259
|
+
try {
|
|
260
|
+
control.meta.phases = JSON.parse(args[i + 1]);
|
|
261
|
+
} catch (_e) {
|
|
262
|
+
console.error("Invalid phases JSON.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
i += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
config.saveControl(context, control);
|
|
270
|
+
if (config.isOperaInstalled(control)) {
|
|
271
|
+
installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
|
|
272
|
+
env.syncEnvironment(context, control);
|
|
273
|
+
const ops = require("./control");
|
|
274
|
+
ops.syncDocs(context, control);
|
|
167
275
|
}
|
|
168
|
-
|
|
169
|
-
config.saveControl(root, control);
|
|
170
276
|
console.log("Configuration updated.");
|
|
171
277
|
}
|
|
172
278
|
|
|
173
279
|
function upgrade(root) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
|
|
187
|
-
copyDirRecursive(refsTemplateDir, starterSkillDir);
|
|
188
|
-
}
|
|
189
|
-
|
|
280
|
+
const context = config.ensureContext(root);
|
|
281
|
+
const control = config.loadControl(context);
|
|
282
|
+
const locale = config.getLocale(control);
|
|
283
|
+
setLocale(locale);
|
|
284
|
+
|
|
285
|
+
if (!config.isOperaInstalled(control)) {
|
|
286
|
+
console.log(t("opera.notInstalled"));
|
|
287
|
+
console.log("Run 'trackops opera install' first.");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
|
|
190
292
|
control.meta.opera.version = OPERA_VERSION;
|
|
191
|
-
config.saveControl(
|
|
293
|
+
config.saveControl(context, control);
|
|
294
|
+
env.syncEnvironment(context, control);
|
|
192
295
|
console.log(t("opera.upgraded", { version: OPERA_VERSION }));
|
|
193
296
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
297
|
+
|
|
298
|
+
function cmdInstall(root, args) {
|
|
299
|
+
const options = {
|
|
300
|
+
bootstrap: true,
|
|
301
|
+
answers: {},
|
|
302
|
+
interactive: true,
|
|
303
|
+
locale: null,
|
|
304
|
+
};
|
|
305
|
+
for (let i = 0; i < (args || []).length; i += 1) {
|
|
306
|
+
if (args[i] === "--locale" && args[i + 1]) {
|
|
307
|
+
options.locale = args[i + 1];
|
|
308
|
+
i += 1;
|
|
309
|
+
} else if (args[i] === "--no-bootstrap") {
|
|
310
|
+
options.bootstrap = false;
|
|
311
|
+
} else if (args[i] === "--non-interactive") {
|
|
312
|
+
options.interactive = false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return install(root, options);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cmdStatus(root) { status(root); }
|
|
319
|
+
function cmdConfigure(root, args) { configure(root, args); }
|
|
320
|
+
function cmdUpgrade(root) { upgrade(root); }
|
|
321
|
+
|
|
322
|
+
async function cmdBootstrap(root, args) {
|
|
323
|
+
const options = { locale: null, interactive: true, answers: {} };
|
|
324
|
+
for (let i = 0; i < (args || []).length; i += 1) {
|
|
325
|
+
if (args[i] === "--locale" && args[i + 1]) {
|
|
326
|
+
options.locale = args[i + 1];
|
|
327
|
+
i += 1;
|
|
328
|
+
} else if (args[i] === "--non-interactive") {
|
|
329
|
+
options.interactive = false;
|
|
330
|
+
} else if (args[i] === "--skip-repo-tasks") {
|
|
331
|
+
options.answers.repoTaskPolicy = "skip";
|
|
332
|
+
} else if (args[i] === "--include-repo-tasks") {
|
|
333
|
+
options.answers.repoTaskPolicy = "optional_pending";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return runBootstrap(root, options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
installStructure,
|
|
341
|
+
install,
|
|
342
|
+
runBootstrap,
|
|
343
|
+
status,
|
|
344
|
+
configure,
|
|
345
|
+
upgrade,
|
|
346
|
+
cmdInstall,
|
|
347
|
+
cmdStatus,
|
|
348
|
+
cmdConfigure,
|
|
349
|
+
cmdUpgrade,
|
|
350
|
+
cmdBootstrap,
|
|
351
|
+
};
|
package/lib/registry.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
|
|
7
|
+
const config = require("./config");
|
|
7
8
|
const { t } = require("./i18n");
|
|
8
9
|
|
|
9
10
|
const REGISTRY_DIR = path.join(os.homedir(), ".codex", "trackops");
|
|
@@ -39,9 +40,11 @@ function ensureRegistryDir() {
|
|
|
39
40
|
function loadRegistry() {
|
|
40
41
|
ensureRegistryDir();
|
|
41
42
|
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
42
|
-
return { version:
|
|
43
|
+
return { version: 2, updatedAt: nowIso(), projects: [] };
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
const registry = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8"));
|
|
46
|
+
if (!registry.version) registry.version = 1;
|
|
47
|
+
return registry;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
function saveRegistry(registry) {
|
|
@@ -51,19 +54,21 @@ function saveRegistry(registry) {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function isProjectInstalled(rootDir) {
|
|
54
|
-
|
|
57
|
+
const context = config.resolveWorkspaceContext(rootDir);
|
|
58
|
+
return Boolean(context && fs.existsSync(context.controlFile));
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
function inspectProject(rootDir) {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
62
|
+
const context = config.resolveWorkspaceContext(rootDir) || config.createLegacyContext(rootDir);
|
|
63
|
+
const root = context.workspaceRoot;
|
|
64
|
+
const controlStateFile = context.controlFile;
|
|
60
65
|
|
|
61
66
|
if (!fs.existsSync(controlStateFile)) {
|
|
62
67
|
throw new Error(`Project '${root}' does not have trackops installed.`);
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
let packageJson = {};
|
|
66
|
-
const packageFile =
|
|
71
|
+
const packageFile = context.packageFile;
|
|
67
72
|
if (fs.existsSync(packageFile)) {
|
|
68
73
|
packageJson = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
69
74
|
}
|
|
@@ -76,6 +81,10 @@ function inspectProject(rootDir) {
|
|
|
76
81
|
id: `${slugify(projectName) || "project"}-${shortHash(root)}`,
|
|
77
82
|
name: projectName,
|
|
78
83
|
root,
|
|
84
|
+
workspaceRoot: root,
|
|
85
|
+
appRoot: context.appRoot,
|
|
86
|
+
opsRoot: context.opsRoot,
|
|
87
|
+
layout: context.layout,
|
|
79
88
|
packageName: packageJson.name || null,
|
|
80
89
|
controlStateFile,
|
|
81
90
|
registeredAt: nowIso(),
|
|
@@ -86,7 +95,7 @@ function inspectProject(rootDir) {
|
|
|
86
95
|
function registerProject(rootDir) {
|
|
87
96
|
const registry = loadRegistry();
|
|
88
97
|
const entry = inspectProject(rootDir);
|
|
89
|
-
const existingIndex = registry.projects.findIndex((p) => p.root === entry.
|
|
98
|
+
const existingIndex = registry.projects.findIndex((p) => (p.workspaceRoot || p.root) === entry.workspaceRoot);
|
|
90
99
|
|
|
91
100
|
if (existingIndex >= 0) {
|
|
92
101
|
registry.projects[existingIndex] = {
|
|
@@ -101,13 +110,14 @@ function registerProject(rootDir) {
|
|
|
101
110
|
|
|
102
111
|
registry.projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
112
|
saveRegistry(registry);
|
|
104
|
-
return registry.projects.find((p) => p.root === entry.
|
|
113
|
+
return registry.projects.find((p) => (p.workspaceRoot || p.root) === entry.workspaceRoot);
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
function unregisterProject(rootDir) {
|
|
108
|
-
const
|
|
117
|
+
const context = config.resolveWorkspaceContext(rootDir) || config.createLegacyContext(rootDir);
|
|
118
|
+
const root = context.workspaceRoot;
|
|
109
119
|
const registry = loadRegistry();
|
|
110
|
-
registry.projects = registry.projects.filter((p) => p.root !== root);
|
|
120
|
+
registry.projects = registry.projects.filter((p) => (p.workspaceRoot || p.root) !== root);
|
|
111
121
|
saveRegistry(registry);
|
|
112
122
|
return registry;
|
|
113
123
|
}
|
|
@@ -125,7 +135,9 @@ function resolveProject(projectRef, fallbackRoot) {
|
|
|
125
135
|
|
|
126
136
|
if (!projectRef) {
|
|
127
137
|
if (fallbackRoot) {
|
|
128
|
-
|
|
138
|
+
const resolved = config.resolveWorkspaceContext(fallbackRoot);
|
|
139
|
+
const target = resolved ? resolved.workspaceRoot : path.resolve(fallbackRoot);
|
|
140
|
+
return projects.find((p) => (p.workspaceRoot || p.root) === target) || null;
|
|
129
141
|
}
|
|
130
142
|
return projects[0] || null;
|
|
131
143
|
}
|
|
@@ -133,7 +145,7 @@ function resolveProject(projectRef, fallbackRoot) {
|
|
|
133
145
|
const normalizedRef = path.resolve(projectRef);
|
|
134
146
|
return (
|
|
135
147
|
projects.find((p) => p.id === projectRef) ||
|
|
136
|
-
projects.find((p) => p.root === normalizedRef) ||
|
|
148
|
+
projects.find((p) => (p.workspaceRoot || p.root) === normalizedRef) ||
|
|
137
149
|
null
|
|
138
150
|
);
|
|
139
151
|
}
|
|
@@ -155,7 +167,9 @@ function cmdList() {
|
|
|
155
167
|
projects.forEach((project, index) => {
|
|
156
168
|
console.log(`${index + 1}. ${project.name}`);
|
|
157
169
|
console.log(` id: ${project.id}`);
|
|
158
|
-
console.log(` root: ${project.root}`);
|
|
170
|
+
console.log(` root: ${project.workspaceRoot || project.root}`);
|
|
171
|
+
console.log(` app: ${project.appRoot}`);
|
|
172
|
+
console.log(` ops: ${project.opsRoot}`);
|
|
159
173
|
console.log(` available: ${project.available ? "yes" : "no"}`);
|
|
160
174
|
});
|
|
161
175
|
}
|