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.
Files changed (57) hide show
  1. package/README.md +326 -270
  2. package/bin/trackops.js +102 -70
  3. package/lib/config.js +260 -35
  4. package/lib/control.js +517 -475
  5. package/lib/env.js +227 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +135 -46
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +523 -0
  10. package/lib/opera.js +319 -170
  11. package/lib/registry.js +27 -13
  12. package/lib/release.js +56 -0
  13. package/lib/resources.js +42 -0
  14. package/lib/server.js +907 -554
  15. package/lib/skills.js +148 -124
  16. package/lib/workspace.js +260 -0
  17. package/locales/en.json +331 -139
  18. package/locales/es.json +331 -139
  19. package/package.json +7 -9
  20. package/scripts/skills-marketplace-smoke.js +124 -0
  21. package/scripts/smoke-tests.js +445 -0
  22. package/scripts/sync-skill-version.js +21 -0
  23. package/scripts/validate-skill.js +88 -0
  24. package/skills/trackops/SKILL.md +64 -0
  25. package/skills/trackops/agents/openai.yaml +3 -0
  26. package/skills/trackops/references/activation.md +39 -0
  27. package/skills/trackops/references/troubleshooting.md +34 -0
  28. package/skills/trackops/references/workflow.md +20 -0
  29. package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
  30. package/skills/trackops/skill.json +29 -0
  31. package/templates/opera/en/agent.md +26 -0
  32. package/templates/opera/en/genesis.md +79 -0
  33. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  34. package/templates/opera/en/references/opera-cycle.md +62 -0
  35. package/templates/opera/en/registry.md +28 -0
  36. package/templates/opera/en/router.md +39 -0
  37. package/templates/opera/genesis.md +79 -94
  38. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  39. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  40. package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
  41. package/ui/css/panels.css +956 -953
  42. package/ui/index.html +1 -1
  43. package/ui/js/api.js +211 -194
  44. package/ui/js/app.js +200 -199
  45. package/ui/js/i18n.js +14 -0
  46. package/ui/js/onboarding.js +439 -437
  47. package/ui/js/state.js +130 -129
  48. package/ui/js/utils.js +175 -172
  49. package/ui/js/views/board.js +255 -254
  50. package/ui/js/views/execution.js +256 -256
  51. package/ui/js/views/insights.js +340 -339
  52. package/ui/js/views/overview.js +365 -364
  53. package/ui/js/views/settings.js +340 -202
  54. package/ui/js/views/sidebar.js +131 -132
  55. package/ui/js/views/skills.js +163 -162
  56. package/ui/js/views/tasks.js +406 -405
  57. 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 copyTemplate(templatePath, targetPath, replacements) {
18
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
19
- let content = fs.readFileSync(templatePath, "utf8");
20
- if (replacements) {
21
- for (const [key, value] of Object.entries(replacements)) {
22
- content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
23
- }
24
- }
25
- fs.writeFileSync(targetPath, content, "utf8");
26
- }
27
-
28
- function copyDirRecursive(src, dest) {
29
- fs.mkdirSync(dest, { recursive: true });
30
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
31
- const srcPath = path.join(src, entry.name);
32
- const destPath = path.join(dest, entry.name);
33
- if (entry.isDirectory()) {
34
- copyDirRecursive(srcPath, destPath);
35
- } else {
36
- fs.copyFileSync(srcPath, destPath);
37
- }
38
- }
39
- }
40
-
41
- function install(root, options) {
42
- const controlFile = config.controlFilePath(root);
43
- if (!fs.existsSync(controlFile)) {
44
- throw new Error("project_control.json not found. Run 'trackops init' first.");
45
- }
46
-
47
- const control = config.loadControl(root);
48
- setLocale(config.getLocale(control));
49
-
50
- if (config.isOperaInstalled(control)) {
51
- console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) }));
52
- return;
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
- // Create .agent/hub/ with agent.md and router.md
58
- const agentHubDir = path.join(root, ".agent", "hub");
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
- const agentTemplatePath = path.join(TEMPLATES_DIR, "agent.md");
62
- const routerTemplatePath = path.join(TEMPLATES_DIR, "router.md");
63
-
64
- if (fs.existsSync(agentTemplatePath) && !fs.existsSync(path.join(agentHubDir, "agent.md"))) {
65
- copyTemplate(agentTemplatePath, path.join(agentHubDir, "agent.md"), { PROJECT_NAME: projectName });
66
- }
67
- if (fs.existsSync(routerTemplatePath) && !fs.existsSync(path.join(agentHubDir, "router.md"))) {
68
- copyTemplate(routerTemplatePath, path.join(agentHubDir, "router.md"), { PROJECT_NAME: projectName });
69
- }
70
-
71
- // Create .agents/skills/_registry.md
72
- const skillsRegistryDir = path.join(root, ".agents", "skills");
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
- const registryPath = path.join(skillsRegistryDir, "_registry.md");
75
- const registryTemplatePath = path.join(TEMPLATES_DIR, "registry.md");
76
- if (fs.existsSync(registryTemplatePath) && !fs.existsSync(registryPath)) {
77
- copyTemplate(registryTemplatePath, registryPath, { PROJECT_NAME: projectName });
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
- // Create genesis.md if not exists
81
- const genesisTemplatePath = path.join(TEMPLATES_DIR, "genesis.md");
82
- const genesisPath = path.join(root, "genesis.md");
83
- if (fs.existsSync(genesisTemplatePath) && !fs.existsSync(genesisPath)) {
84
- copyTemplate(genesisTemplatePath, genesisPath, { PROJECT_NAME: projectName });
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
- // Copy OPERA references
88
- const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
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
- installed: true,
105
- version: OPERA_VERSION,
106
- installedAt: nowIso(),
107
- skills: [],
108
- };
109
- config.saveControl(root, control);
110
-
111
- console.log(t("opera.installed", { version: OPERA_VERSION }));
112
-
113
- // Auto-install base skills (commiter, changelog-updater)
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 { skills.installSkill(root, skillName); }
117
- catch (_e) { /* already installed or not in catalog — skip silently */ }
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 (!config.isOperaInstalled(control)) {
126
- console.log(t("opera.notInstalled"));
127
- return;
178
+ if (options.bootstrap !== false) {
179
+ await runBootstrap(context, { locale, answers: options.answers, interactive: options.interactive });
128
180
  }
181
+ }
129
182
 
130
- const opera = control.meta.opera;
131
- console.log(`OPERA v${opera.version}`);
132
- console.log(` Installed: ${opera.installedAt}`);
133
- console.log(` Skills: ${(opera.skills || []).join(", ") || "none"}`);
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
- // Check structural integrity
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(root, ".agent", "hub", "agent.md"))],
138
- [".agent/hub/router.md", fs.existsSync(path.join(root, ".agent", "hub", "router.md"))],
139
- [".agents/skills/_registry.md", fs.existsSync(path.join(root, ".agents", "skills", "_registry.md"))],
140
- ["genesis.md", fs.existsSync(path.join(root, "genesis.md"))],
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 control = config.loadControl(root);
151
- setLocale(config.getLocale(control));
152
-
153
- for (let i = 0; i < args.length; i += 1) {
154
- if (args[i] === "--locale" && args[i + 1]) {
155
- control.meta.locale = args[i + 1];
156
- i += 1;
157
- }
158
- if (args[i] === "--phases" && args[i + 1]) {
159
- try {
160
- control.meta.phases = JSON.parse(args[i + 1]);
161
- } catch (_e) {
162
- console.error("Invalid phases JSON.");
163
- return;
164
- }
165
- i += 1;
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 control = config.loadControl(root);
175
- setLocale(config.getLocale(control));
176
-
177
- if (!config.isOperaInstalled(control)) {
178
- console.log(t("opera.notInstalled"));
179
- console.log("Run 'trackops opera install' first.");
180
- return;
181
- }
182
-
183
- // Re-copy reference files
184
- const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
185
- if (fs.existsSync(refsTemplateDir)) {
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(root, control);
293
+ config.saveControl(context, control);
294
+ env.syncEnvironment(context, control);
192
295
  console.log(t("opera.upgraded", { version: OPERA_VERSION }));
193
296
  }
194
-
195
- /* ── CLI commands ── */
196
-
197
- function cmdInstall(root, args) { install(root, {}); }
198
- function cmdStatus(root) { status(root); }
199
- function cmdConfigure(root, args) { configure(root, args); }
200
- function cmdUpgrade(root) { upgrade(root); }
201
-
202
- module.exports = { install, status, configure, upgrade, cmdInstall, cmdStatus, cmdConfigure, cmdUpgrade };
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: 1, updatedAt: nowIso(), projects: [] };
43
+ return { version: 2, updatedAt: nowIso(), projects: [] };
43
44
  }
44
- return JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8"));
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
- return fs.existsSync(path.join(rootDir, "project_control.json"));
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 root = path.resolve(rootDir);
59
- const controlStateFile = path.join(root, "project_control.json");
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 = path.join(root, "package.json");
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.root);
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.root);
113
+ return registry.projects.find((p) => (p.workspaceRoot || p.root) === entry.workspaceRoot);
105
114
  }
106
115
 
107
116
  function unregisterProject(rootDir) {
108
- const root = path.resolve(rootDir);
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
- return projects.find((p) => p.root === path.resolve(fallbackRoot)) || null;
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
  }