trackops 2.0.3 → 2.0.5

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 (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +695 -402
  3. package/bin/trackops.js +116 -116
  4. package/lib/config.js +326 -326
  5. package/lib/control.js +208 -208
  6. package/lib/env.js +244 -244
  7. package/lib/init.js +325 -325
  8. package/lib/locale.js +24 -0
  9. package/lib/opera-bootstrap.js +941 -874
  10. package/lib/opera.js +494 -477
  11. package/lib/preferences.js +74 -74
  12. package/lib/registry.js +214 -196
  13. package/lib/release.js +56 -56
  14. package/lib/runtime-state.js +144 -144
  15. package/lib/server.js +312 -207
  16. package/lib/skills.js +74 -57
  17. package/lib/workspace.js +260 -260
  18. package/locales/en.json +192 -166
  19. package/locales/es.json +192 -166
  20. package/package.json +61 -58
  21. package/scripts/postinstall-locale.js +21 -21
  22. package/scripts/skills-marketplace-smoke.js +124 -124
  23. package/scripts/smoke-tests.js +558 -554
  24. package/scripts/sync-skill-version.js +21 -21
  25. package/scripts/validate-skill.js +103 -103
  26. package/skills/trackops/SKILL.md +126 -122
  27. package/skills/trackops/agents/openai.yaml +7 -7
  28. package/skills/trackops/locales/en/SKILL.md +126 -122
  29. package/skills/trackops/locales/en/references/activation.md +94 -75
  30. package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
  31. package/skills/trackops/locales/en/references/workflow.md +55 -32
  32. package/skills/trackops/references/activation.md +94 -75
  33. package/skills/trackops/references/troubleshooting.md +73 -55
  34. package/skills/trackops/references/workflow.md +55 -32
  35. package/skills/trackops/skill.json +29 -29
  36. package/templates/hooks/post-checkout +2 -2
  37. package/templates/hooks/post-commit +2 -2
  38. package/templates/hooks/post-merge +2 -2
  39. package/templates/opera/agent.md +28 -27
  40. package/templates/opera/architecture/dependency-graph.md +24 -24
  41. package/templates/opera/architecture/runtime-automation.md +24 -24
  42. package/templates/opera/architecture/runtime-operations.md +34 -34
  43. package/templates/opera/en/agent.md +22 -21
  44. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  45. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  46. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  47. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  48. package/templates/opera/en/reviews/integration-audit.md +18 -18
  49. package/templates/opera/en/router.md +24 -19
  50. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  51. package/templates/opera/references/opera-cycle.md +193 -193
  52. package/templates/opera/registry.md +28 -28
  53. package/templates/opera/reviews/delivery-audit.md +18 -18
  54. package/templates/opera/reviews/integration-audit.md +18 -18
  55. package/templates/opera/router.md +54 -49
  56. package/templates/skills/changelog-updater/SKILL.md +69 -69
  57. package/templates/skills/commiter/SKILL.md +99 -99
  58. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  59. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  60. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  61. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  62. package/templates/skills/opera-skill/SKILL.md +279 -0
  63. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  65. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  66. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  67. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  68. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  69. package/ui/css/base.css +284 -266
  70. package/ui/css/charts.css +425 -327
  71. package/ui/css/components.css +1107 -570
  72. package/ui/css/onboarding.css +133 -0
  73. package/ui/css/panels.css +345 -406
  74. package/ui/css/terminal.css +125 -0
  75. package/ui/css/timeline.css +58 -0
  76. package/ui/css/tokens.css +284 -227
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -96
  79. package/ui/js/api.js +49 -13
  80. package/ui/js/app.js +28 -32
  81. package/ui/js/charts.js +526 -0
  82. package/ui/js/console-logger.js +172 -172
  83. package/ui/js/filters.js +247 -0
  84. package/ui/js/icons.js +129 -104
  85. package/ui/js/keyboard.js +229 -0
  86. package/ui/js/onboarding.js +33 -42
  87. package/ui/js/router.js +142 -125
  88. package/ui/js/theme.js +100 -100
  89. package/ui/js/time-tracker.js +248 -248
  90. package/ui/js/views/board.js +84 -114
  91. package/ui/js/views/dashboard.js +870 -0
  92. package/ui/js/views/flash.js +47 -47
  93. package/ui/js/views/projects.js +745 -0
  94. package/ui/js/views/scrum.js +476 -0
  95. package/ui/js/views/settings.js +153 -203
  96. package/ui/js/views/sidebar.js +37 -31
  97. package/ui/js/views/tasks.js +218 -101
  98. package/ui/js/views/timeline.js +265 -0
  99. package/ui/js/views/topbar.js +94 -107
  100. package/ui/app.js +0 -950
  101. package/ui/js/views/insights.js +0 -340
  102. package/ui/js/views/overview.js +0 -369
  103. package/ui/styles.css +0 -688
package/lib/opera.js CHANGED
@@ -3,26 +3,26 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
 
6
- const config = require("./config");
7
- const env = require("./env");
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");
12
- const runtimeState = require("./runtime-state");
13
-
14
- const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
15
- const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
16
- const OPERA_VERSION = require("../package.json").version;
17
- const AUXILIARY_SKILLS = ["project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
-
19
- function nowIso() {
20
- return new Date().toISOString();
21
- }
22
-
23
- function formatLocaleSource(source) {
24
- return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
- }
6
+ const config = require("./config");
7
+ const env = require("./env");
8
+ const { t, setLocale } = require("./i18n");
9
+ const { promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
10
+ const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
+ const bootstrap = require("./opera-bootstrap");
12
+ const runtimeState = require("./runtime-state");
13
+
14
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
15
+ const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
16
+ const OPERA_VERSION = require("../package.json").version;
17
+ const AUXILIARY_SKILLS = ["opera-skill", "project-starter-skill", "opera-contract-auditor", "opera-policy-guard"];
18
+
19
+ function nowIso() {
20
+ return new Date().toISOString();
21
+ }
22
+
23
+ function formatLocaleSource(source) {
24
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
+ }
26
26
 
27
27
  function readText(filePath) {
28
28
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -53,42 +53,58 @@ function copyTemplate(templatePath, targetPath, replacements, options = {}) {
53
53
  return true;
54
54
  }
55
55
 
56
- function copyDirRecursive(src, dest) {
57
- if (!src || !fs.existsSync(src)) return;
58
- fs.mkdirSync(dest, { recursive: true });
59
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
60
- const srcPath = path.join(src, entry.name);
56
+ function copyDirRecursive(src, dest) {
57
+ if (!src || !fs.existsSync(src)) return;
58
+ fs.mkdirSync(dest, { recursive: true });
59
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
60
+ const srcPath = path.join(src, entry.name);
61
61
  const destPath = path.join(dest, entry.name);
62
62
  if (entry.isDirectory()) {
63
63
  copyDirRecursive(srcPath, destPath);
64
64
  } else {
65
65
  fs.copyFileSync(srcPath, destPath);
66
- }
67
- }
68
- }
69
-
70
- function seedDirRecursive(src, dest, options = {}) {
71
- if (!src || !fs.existsSync(src)) return;
72
- fs.mkdirSync(dest, { recursive: true });
73
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
74
- const srcPath = path.join(src, entry.name);
75
- const destPath = path.join(dest, entry.name);
76
- if (entry.isDirectory()) {
77
- seedDirRecursive(srcPath, destPath, options);
78
- } else if (options.overwrite || !fs.existsSync(destPath)) {
79
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
80
- fs.copyFileSync(srcPath, destPath);
81
- }
82
- }
83
- }
84
-
85
- function resolveOperaLocale(control, options = {}) {
86
- return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
87
- }
88
-
89
- function installStructure(root, control, locale, options = {}) {
90
- const context = config.ensureContext(root);
91
- const projectName = control.meta.projectName || "Project";
66
+ }
67
+ }
68
+ }
69
+
70
+ function seedDirRecursive(src, dest, options = {}) {
71
+ if (!src || !fs.existsSync(src)) return;
72
+ fs.mkdirSync(dest, { recursive: true });
73
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
74
+ const srcPath = path.join(src, entry.name);
75
+ const destPath = path.join(dest, entry.name);
76
+ if (entry.isDirectory()) {
77
+ seedDirRecursive(srcPath, destPath, options);
78
+ } else if (options.overwrite || !fs.existsSync(destPath)) {
79
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
80
+ fs.copyFileSync(srcPath, destPath);
81
+ }
82
+ }
83
+ }
84
+
85
+ function resolveOperaLocale(control, options = {}) {
86
+ return resolveLocale(options.locale, config.getLocale(control) || runtimeState.getGlobalLocale());
87
+ }
88
+
89
+ function seedAuxiliarySkill(skillName, locale, skillsDir, options = {}) {
90
+ const templateDir = path.join(SKILLS_TEMPLATES_DIR, skillName);
91
+ if (!fs.existsSync(templateDir)) return;
92
+ const targetDir = path.join(skillsDir, skillName);
93
+ seedDirRecursive(templateDir, targetDir, {
94
+ overwrite: options.rewriteLocalizedTemplates === true,
95
+ });
96
+ const localizedSkill = resolveSkillFile(SKILLS_TEMPLATES_DIR, skillName, locale);
97
+ if (localizedSkill) {
98
+ const targetSkill = path.join(targetDir, "SKILL.md");
99
+ if (!fs.existsSync(targetSkill) || options.rewriteLocalizedTemplates === true) {
100
+ fs.copyFileSync(localizedSkill, targetSkill);
101
+ }
102
+ }
103
+ }
104
+
105
+ function installStructure(root, control, locale, options = {}) {
106
+ const context = config.ensureContext(root);
107
+ const projectName = control.meta.projectName || "Project";
92
108
  const replacements = {
93
109
  PROJECT_NAME: projectName,
94
110
  DESIRED_OUTCOME: t("bootstrap.pendingValue"),
@@ -108,13 +124,13 @@ function installStructure(root, control, locale, options = {}) {
108
124
  overwriteIfTemplate: options.rewriteLocalizedTemplates === true,
109
125
  };
110
126
 
111
- const agentHubDir = context.paths.agentHubDir;
112
- fs.mkdirSync(agentHubDir, { recursive: true });
113
- fs.mkdirSync(context.paths.architectureDir, { recursive: true });
114
- fs.mkdirSync(context.paths.contractDir, { recursive: true });
115
- fs.mkdirSync(context.paths.policyDir, { recursive: true });
116
- fs.mkdirSync(context.paths.bootstrapDir, { recursive: true });
117
- fs.mkdirSync(context.paths.reviewsDir, { recursive: true });
127
+ const agentHubDir = context.paths.agentHubDir;
128
+ fs.mkdirSync(agentHubDir, { recursive: true });
129
+ fs.mkdirSync(context.paths.architectureDir, { recursive: true });
130
+ fs.mkdirSync(context.paths.contractDir, { recursive: true });
131
+ fs.mkdirSync(context.paths.policyDir, { recursive: true });
132
+ fs.mkdirSync(context.paths.bootstrapDir, { recursive: true });
133
+ fs.mkdirSync(context.paths.reviewsDir, { recursive: true });
118
134
 
119
135
  copyTemplate(
120
136
  resolveLocalizedFile(TEMPLATES_DIR, locale, "agent.md"),
@@ -129,8 +145,8 @@ function installStructure(root, control, locale, options = {}) {
129
145
  overwriteOptions,
130
146
  );
131
147
 
132
- const skillsRegistryDir = context.paths.skillsDir;
133
- fs.mkdirSync(skillsRegistryDir, { recursive: true });
148
+ const skillsRegistryDir = context.paths.skillsDir;
149
+ fs.mkdirSync(skillsRegistryDir, { recursive: true });
134
150
  copyTemplate(
135
151
  resolveLocalizedFile(TEMPLATES_DIR, locale, "registry.md"),
136
152
  path.join(skillsRegistryDir, "_registry.md"),
@@ -139,77 +155,72 @@ function installStructure(root, control, locale, options = {}) {
139
155
  );
140
156
 
141
157
  const genesisTemplatePath = resolveLocalizedFile(TEMPLATES_DIR, locale, "genesis.md");
142
- const genesisPath = context.paths.genesisFile;
158
+ const genesisPath = context.paths.genesisFile;
143
159
  if (!fs.existsSync(genesisPath) || bootstrap.isVirginGenesis(readText(genesisPath))) {
144
160
  copyTemplate(genesisTemplatePath, genesisPath, replacements, { overwrite: true });
145
161
  }
146
162
 
147
- const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
148
- const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
149
- seedDirRecursive(refsTemplateDir, refsTargetDir, {
150
- overwrite: options.rewriteLocalizedTemplates === true,
151
- });
152
-
153
- const architectureTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "architecture");
154
- seedDirRecursive(architectureTemplateDir, context.paths.architectureDir, {
155
- overwrite: options.rewriteLocalizedTemplates === true,
156
- });
157
-
158
- const reviewsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "reviews");
159
- seedDirRecursive(reviewsTemplateDir, context.paths.reviewsDir, {
160
- overwrite: options.rewriteLocalizedTemplates === true,
161
- });
162
-
163
- for (const skillName of AUXILIARY_SKILLS) {
164
- const skillTarget = path.join(context.paths.skillsDir, skillName, "SKILL.md");
165
- const skillTemplate = resolveSkillFile(SKILLS_TEMPLATES_DIR, skillName, locale);
166
- if (skillTemplate) {
167
- fs.mkdirSync(path.dirname(skillTarget), { recursive: true });
168
- if (!fs.existsSync(skillTarget) || options.rewriteLocalizedTemplates === true) {
169
- fs.copyFileSync(skillTemplate, skillTarget);
170
- }
171
- }
172
- }
173
-
174
- bootstrap.writeAutonomyPolicy(context);
175
- }
176
-
177
- async function install(root, options = {}) {
178
- const context = config.ensureContext(root);
179
- const controlFile = config.controlFilePath(context);
180
- if (!fs.existsSync(controlFile)) {
181
- throw new Error("project_control.json not found. Run 'trackops init' first.");
182
- }
183
-
184
- const control = config.loadControl(context);
185
- let locale = resolveOperaLocale(control, options);
186
- if (!options.locale && !control.meta?.locale && !runtimeState.getGlobalLocale()) {
187
- locale = await promptForLocale(locale);
188
- if (!runtimeState.getGlobalLocale()) {
189
- await runtimeState.ensureGlobalLocale({ preferredLocale: locale, interactive: false });
190
- }
191
- }
163
+ const refsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "references");
164
+ const refsTargetDir = path.join(context.paths.skillsDir, "project-starter-skill", "references");
165
+ seedDirRecursive(refsTemplateDir, refsTargetDir, {
166
+ overwrite: options.rewriteLocalizedTemplates === true,
167
+ });
168
+
169
+ const architectureTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "architecture");
170
+ seedDirRecursive(architectureTemplateDir, context.paths.architectureDir, {
171
+ overwrite: options.rewriteLocalizedTemplates === true,
172
+ });
173
+
174
+ const reviewsTemplateDir = resolveLocalizedDir(TEMPLATES_DIR, locale, "reviews");
175
+ seedDirRecursive(reviewsTemplateDir, context.paths.reviewsDir, {
176
+ overwrite: options.rewriteLocalizedTemplates === true,
177
+ });
178
+
179
+ for (const skillName of AUXILIARY_SKILLS) {
180
+ seedAuxiliarySkill(skillName, locale, context.paths.skillsDir, options);
181
+ }
182
+
183
+ bootstrap.writeAutonomyPolicy(context);
184
+ }
185
+
186
+ async function install(root, options = {}) {
187
+ const context = config.ensureContext(root);
188
+ const controlFile = config.controlFilePath(context);
189
+ if (!fs.existsSync(controlFile)) {
190
+ throw new Error("project_control.json not found. Run 'trackops init' first.");
191
+ }
192
+
193
+ const control = config.loadControl(context);
194
+ let locale = resolveOperaLocale(control, options);
195
+ if (!options.locale && !control.meta?.locale && !runtimeState.getGlobalLocale()) {
196
+ locale = await promptForLocale(locale);
197
+ if (!runtimeState.getGlobalLocale()) {
198
+ await runtimeState.ensureGlobalLocale({ preferredLocale: locale, interactive: false });
199
+ }
200
+ } else if (!options.locale) {
201
+ locale = await maybePromptForLocale(locale, { promptMode: "always" });
202
+ }
192
203
  control.meta.locale = locale;
193
- setLocale(locale);
194
-
195
- const alreadyInstalled = config.isOperaInstalled(control);
196
- installStructure(context, control, locale);
197
-
198
- control.meta.opera = {
199
- ...(control.meta.opera || {}),
200
- installed: true,
201
- model: "v3",
202
- stableTag: "stable",
203
- version: OPERA_VERSION,
204
- installedAt: control.meta?.opera?.installedAt || nowIso(),
205
- skills: control.meta?.opera?.skills || [],
206
- legacyStatus: "supported",
207
- };
208
- if (!control.meta.opera.bootstrap && options.bootstrap === false) {
209
- control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
210
- }
211
- config.saveControl(context, control);
212
- env.syncEnvironment(context, control);
204
+ setLocale(locale);
205
+
206
+ const alreadyInstalled = config.isOperaInstalled(control);
207
+ installStructure(context, control, locale);
208
+
209
+ control.meta.opera = {
210
+ ...(control.meta.opera || {}),
211
+ installed: true,
212
+ model: "v3",
213
+ stableTag: "stable",
214
+ version: OPERA_VERSION,
215
+ installedAt: control.meta?.opera?.installedAt || nowIso(),
216
+ skills: control.meta?.opera?.skills || [],
217
+ legacyStatus: "supported",
218
+ };
219
+ if (!control.meta.opera.bootstrap && options.bootstrap === false) {
220
+ control.meta.opera.bootstrap = bootstrap.createAwaitingBootstrapState(context);
221
+ }
222
+ config.saveControl(context, control);
223
+ env.syncEnvironment(context, control);
213
224
 
214
225
  if (!alreadyInstalled) {
215
226
  console.log(t("opera.installed", { version: OPERA_VERSION }));
@@ -217,170 +228,176 @@ async function install(root, options = {}) {
217
228
  console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
218
229
  }
219
230
 
220
- const skills = require("./skills");
221
- for (const skillName of ["commiter", "changelog-updater"]) {
222
- try {
223
- skills.installSkill(context, skillName, { locale });
224
- } catch (_error) {
225
- // ignore
226
- }
227
- }
228
- skills.updateRegistry(context);
229
-
230
- if (options.bootstrap !== false) {
231
- await runBootstrap(context, {
232
- locale,
233
- answers: options.answers,
234
- interactive: options.interactive,
235
- bootstrapMode: options.bootstrapMode,
236
- technicalLevel: options.technicalLevel,
237
- projectState: options.projectState,
238
- docsState: options.docsState,
239
- decisionOwnership: options.decisionOwnership,
240
- });
241
- }
242
- }
243
-
244
- function removePath(targetPath) {
245
- if (!targetPath || !fs.existsSync(targetPath)) return;
246
- fs.rmSync(targetPath, { recursive: true, force: true });
247
- }
248
-
249
- function backupManagedArtifacts(context) {
250
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
251
- const backupRoot = path.join(context.paths.tmpDir, "upgrade-backups", timestamp);
252
- const items = [
253
- context.paths.agentHubDir,
254
- context.paths.skillsDir,
255
- context.paths.genesisFile,
256
- context.paths.contractFile,
257
- context.paths.autonomyPolicyFile,
258
- context.paths.bootstrapDir,
259
- ];
260
- fs.mkdirSync(backupRoot, { recursive: true });
261
- let copied = 0;
262
- for (const item of items) {
263
- if (!fs.existsSync(item)) continue;
264
- const relative = path.relative(context.workspaceRoot, item);
265
- const destination = path.join(backupRoot, relative);
266
- if (fs.statSync(item).isDirectory()) {
267
- copyDirRecursive(item, destination);
268
- } else {
269
- fs.mkdirSync(path.dirname(destination), { recursive: true });
270
- fs.copyFileSync(item, destination);
271
- }
272
- copied += 1;
273
- }
274
- return { backupRoot, copied };
275
- }
276
-
277
- async function runBootstrap(root, options = {}) {
278
- const context = config.ensureContext(root);
279
- const control = config.loadControl(context);
280
- const locale = resolveOperaLocale(control, options);
231
+ const skills = require("./skills");
232
+ for (const skillName of ["commiter", "changelog-updater"]) {
233
+ try {
234
+ skills.installSkill(context, skillName, { locale });
235
+ } catch (_error) {
236
+ // ignore
237
+ }
238
+ }
239
+ skills.updateRegistry(context);
240
+
241
+ if (options.bootstrap !== false) {
242
+ await runBootstrap(context, {
243
+ locale,
244
+ answers: options.answers,
245
+ interactive: options.interactive,
246
+ bootstrapMode: options.bootstrapMode,
247
+ technicalLevel: options.technicalLevel,
248
+ projectState: options.projectState,
249
+ docsState: options.docsState,
250
+ decisionOwnership: options.decisionOwnership,
251
+ });
252
+ }
253
+ }
254
+
255
+ function removePath(targetPath) {
256
+ if (!targetPath || !fs.existsSync(targetPath)) return;
257
+ fs.rmSync(targetPath, { recursive: true, force: true });
258
+ }
259
+
260
+ function backupManagedArtifacts(context) {
261
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
262
+ const backupRoot = path.join(context.paths.tmpDir, "upgrade-backups", timestamp);
263
+ const items = [
264
+ context.paths.agentHubDir,
265
+ context.paths.skillsDir,
266
+ context.paths.genesisFile,
267
+ context.paths.contractFile,
268
+ context.paths.autonomyPolicyFile,
269
+ context.paths.bootstrapDir,
270
+ ];
271
+ fs.mkdirSync(backupRoot, { recursive: true });
272
+ let copied = 0;
273
+ for (const item of items) {
274
+ if (!fs.existsSync(item)) continue;
275
+ const relative = path.relative(context.workspaceRoot, item);
276
+ const destination = path.join(backupRoot, relative);
277
+ if (fs.statSync(item).isDirectory()) {
278
+ copyDirRecursive(item, destination);
279
+ } else {
280
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
281
+ fs.copyFileSync(item, destination);
282
+ }
283
+ copied += 1;
284
+ }
285
+ return { backupRoot, copied };
286
+ }
287
+
288
+ async function runBootstrap(root, options = {}) {
289
+ const context = config.ensureContext(root);
290
+ const control = config.loadControl(context);
291
+ const locale = resolveOperaLocale(control, options);
281
292
  control.meta.locale = locale;
282
293
  setLocale(locale);
283
294
 
284
- const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
285
- if (legacyBootstrap && !control.meta.opera?.bootstrap) {
286
- control.meta.opera = control.meta.opera || {};
287
- control.meta.opera.bootstrap = legacyBootstrap;
288
- config.saveControl(context, control);
289
- }
290
-
291
- if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
292
- const resumed = bootstrap.resumeBootstrap(context, control);
293
- if (resumed.resumed) {
294
- const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
295
- env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
296
- const ops = require("./control");
297
- ops.syncDocs(context, updatedControl);
298
- ops.refreshRepoRuntime(context, { quiet: true });
299
- console.log(t("bootstrap.completed"));
300
- return resumed.profile;
301
- }
302
- console.log(t("bootstrap.awaitingAgent"));
303
- return control.meta.opera.bootstrap;
304
- }
305
-
306
- const profile = await bootstrap.collectBootstrapProfile(context, control, options);
307
- const updatedControl = bootstrap.applyBootstrap(context, control, profile);
308
- env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
309
- const ops = require("./control");
310
- ops.syncDocs(context, updatedControl);
311
- ops.refreshRepoRuntime(context, { quiet: true });
312
-
313
- if (profile.mode === "agent_handoff") {
314
- console.log(t("bootstrap.awaitingAgent"));
315
- console.log(`${t("bootstrap.handoffFile")}: ${profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
316
- } else {
317
- console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
318
- }
319
- return profile;
320
- }
321
-
322
- function status(root) {
323
- const context = config.ensureContext(root);
324
- const control = config.loadControl(context);
325
- setLocale(config.getLocale(control));
295
+ const legacyBootstrap = bootstrap.detectLegacyBootstrap(context, control);
296
+ if (legacyBootstrap && !control.meta.opera?.bootstrap) {
297
+ control.meta.opera = control.meta.opera || {};
298
+ control.meta.opera.bootstrap = legacyBootstrap;
299
+ config.saveControl(context, control);
300
+ }
301
+
302
+ if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
303
+ const resumed = bootstrap.resumeBootstrap(context, control);
304
+ if (resumed.resumed) {
305
+ const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
306
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
307
+ const ops = require("./control");
308
+ ops.syncDocs(context, updatedControl);
309
+ ops.refreshRepoRuntime(context, { quiet: true });
310
+ console.log(t("bootstrap.completed"));
311
+ return resumed.profile;
312
+ }
313
+ console.log(t("bootstrap.awaitingAgent"));
314
+ return control.meta.opera.bootstrap;
315
+ }
316
+
317
+ const profile = await bootstrap.collectBootstrapProfile(context, control, options);
318
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
319
+ env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
320
+ const ops = require("./control");
321
+ ops.syncDocs(context, updatedControl);
322
+ ops.refreshRepoRuntime(context, { quiet: true });
323
+
324
+ if (profile.mode === "agent_handoff") {
325
+ console.log(t("bootstrap.awaitingAgent"));
326
+ console.log(`${t("bootstrap.handoffFile")}: ${profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
327
+ console.log(t("bootstrap.next.handoff"));
328
+ } else {
329
+ console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
330
+ console.log(
331
+ profile.status === "completed"
332
+ ? t("bootstrap.next.directCompleted")
333
+ : t("bootstrap.next.directPending"),
334
+ );
335
+ }
336
+ return profile;
337
+ }
338
+
339
+ function status(root) {
340
+ const context = config.ensureContext(root);
341
+ const control = config.loadControl(context);
342
+ setLocale(config.getLocale(control));
326
343
 
327
344
  if (!config.isOperaInstalled(control)) {
328
345
  console.log(t("opera.notInstalled"));
329
346
  return;
330
347
  }
331
-
332
- const opera = control.meta.opera;
333
- const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
334
- const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
335
- console.log(t("opera.status.version", { version: opera.version }));
336
- console.log(t("opera.status.installed", { value: opera.installedAt }));
337
- console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
338
- console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
339
- console.log(t("opera.status.legacy", { value: opera.legacyStatus || bootstrapState?.status || "supported" }));
340
- console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
341
- console.log(t("opera.status.contractReadiness", { value: opera.contractReadiness || "hypothesis" }));
342
-
343
- if (bootstrapState) {
344
- console.log(t("opera.status.bootstrap", { value: bootstrapState.status }));
345
- if (bootstrapState.mode) {
346
- console.log(t("opera.status.mode", { value: bootstrapState.mode }));
347
- }
348
- if (bootstrapState.routeReason) {
349
- console.log(t("opera.status.route", { value: bootstrapState.routeReason }));
350
- }
351
- if (bootstrapState.decisionOwnership) {
352
- console.log(t("opera.status.ownership", { value: bootstrapState.decisionOwnership }));
353
- }
354
- if ((bootstrapState.missingFields || []).length) {
355
- console.log(t("opera.status.missing", { value: bootstrapState.missingFields.join(", ") }));
356
- console.log(t("opera.status.resume"));
357
- }
358
- if (bootstrapState.handoffFiles?.markdown) {
359
- console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
360
- }
361
- if (bootstrapState.reviewFiles?.qualityReport) {
362
- console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
363
- }
364
- }
365
-
366
- const checks = [
367
- [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
368
- [context.layout === "split" ? "ops/.agent/hub/router.md" : ".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
369
- [context.layout === "split" ? "ops/.agents/skills/_registry.md" : ".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
370
- [context.layout === "split" ? "ops/genesis.md" : "genesis.md", fs.existsSync(context.paths.genesisFile)],
371
- [context.layout === "split" ? "ops/contract/operating-contract.json" : "contract/operating-contract.json", fs.existsSync(context.paths.contractFile)],
372
- [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
373
- ];
374
-
375
- console.log(t("opera.status.structure"));
348
+
349
+ const opera = control.meta.opera;
350
+ const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
351
+ const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
352
+ console.log(t("opera.status.version", { version: opera.version }));
353
+ console.log(t("opera.status.installed", { value: opera.installedAt }));
354
+ console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
355
+ console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
356
+ console.log(t("opera.status.legacy", { value: opera.legacyStatus || bootstrapState?.status || "supported" }));
357
+ console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
358
+ console.log(t("opera.status.contractReadiness", { value: opera.contractReadiness || "hypothesis" }));
359
+
360
+ if (bootstrapState) {
361
+ console.log(t("opera.status.bootstrap", { value: bootstrapState.status }));
362
+ if (bootstrapState.mode) {
363
+ console.log(t("opera.status.mode", { value: bootstrapState.mode }));
364
+ }
365
+ if (bootstrapState.routeReason) {
366
+ console.log(t("opera.status.route", { value: bootstrapState.routeReason }));
367
+ }
368
+ if (bootstrapState.decisionOwnership) {
369
+ console.log(t("opera.status.ownership", { value: bootstrapState.decisionOwnership }));
370
+ }
371
+ if ((bootstrapState.missingFields || []).length) {
372
+ console.log(t("opera.status.missing", { value: bootstrapState.missingFields.join(", ") }));
373
+ console.log(t("opera.status.resume"));
374
+ }
375
+ if (bootstrapState.handoffFiles?.markdown) {
376
+ console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
377
+ }
378
+ if (bootstrapState.reviewFiles?.qualityReport) {
379
+ console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
380
+ }
381
+ }
382
+
383
+ const checks = [
384
+ [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
385
+ [context.layout === "split" ? "ops/.agent/hub/router.md" : ".agent/hub/router.md", fs.existsSync(path.join(context.paths.agentHubDir, "router.md"))],
386
+ [context.layout === "split" ? "ops/.agents/skills/_registry.md" : ".agents/skills/_registry.md", fs.existsSync(context.paths.registryPath)],
387
+ [context.layout === "split" ? "ops/genesis.md" : "genesis.md", fs.existsSync(context.paths.genesisFile)],
388
+ [context.layout === "split" ? "ops/contract/operating-contract.json" : "contract/operating-contract.json", fs.existsSync(context.paths.contractFile)],
389
+ [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
390
+ ];
391
+
392
+ console.log(t("opera.status.structure"));
376
393
  for (const [file, exists] of checks) {
377
394
  console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
378
395
  }
379
396
  }
380
397
 
381
- function configure(root, args) {
382
- const context = config.ensureContext(root);
383
- const control = config.loadControl(context);
398
+ function configure(root, args) {
399
+ const context = config.ensureContext(root);
400
+ const control = config.loadControl(context);
384
401
  setLocale(config.getLocale(control));
385
402
  let nextLocale = config.getLocale(control);
386
403
 
@@ -390,212 +407,212 @@ function configure(root, args) {
390
407
  control.meta.locale = nextLocale;
391
408
  i += 1;
392
409
  }
393
- if (args[i] === "--phases" && args[i + 1]) {
394
- try {
395
- control.meta.phases = JSON.parse(args[i + 1]);
396
- } catch (_e) {
397
- console.error(t("opera.configure.invalidPhases"));
398
- return;
399
- }
410
+ if (args[i] === "--phases" && args[i + 1]) {
411
+ try {
412
+ control.meta.phases = JSON.parse(args[i + 1]);
413
+ } catch (_e) {
414
+ console.error(t("opera.configure.invalidPhases"));
415
+ return;
416
+ }
400
417
  i += 1;
401
418
  }
402
419
  }
403
420
 
404
- config.saveControl(context, control);
405
- if (config.isOperaInstalled(control)) {
406
- installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
407
- env.syncEnvironment(context, control);
408
- const ops = require("./control");
409
- ops.syncDocs(context, control);
410
- }
411
- console.log(t("opera.configure.updated"));
412
- }
413
-
414
- function upgrade(root, args = []) {
415
- const context = config.ensureContext(root);
416
- const control = config.loadControl(context);
417
- const locale = config.getLocale(control);
418
- setLocale(locale);
419
- const wantsStable = (args || []).includes("--stable");
420
- const wantsReset = (args || []).includes("--reset");
421
-
422
- if (!config.isOperaInstalled(control)) {
423
- console.log(t("opera.notInstalled"));
424
- console.log(t("opera.upgrade.runInstallFirst"));
425
- return;
426
- }
427
-
428
- if (!wantsStable) {
429
- console.log(t("opera.upgrade.usage"));
430
- return;
431
- }
432
-
433
- const legacy = bootstrap.detectLegacyBootstrap(context, control);
434
- const isLegacyUnsupported = legacy?.status === "legacy_unsupported";
435
- if (isLegacyUnsupported && !wantsReset) {
436
- control.meta.opera = control.meta.opera || {};
437
- control.meta.opera.legacyStatus = "legacy_unsupported";
438
- config.saveControl(context, control);
439
- console.log(t("opera.upgrade.legacyUnsupported"));
440
- return;
441
- }
442
-
443
- const backup = backupManagedArtifacts(context);
444
- if (wantsReset) {
445
- removePath(context.paths.agentHubDir);
446
- removePath(context.paths.skillsDir);
447
- removePath(context.paths.genesisFile);
448
- removePath(context.paths.contractFile);
449
- removePath(context.paths.autonomyPolicyFile);
450
- removePath(context.paths.bootstrapDir);
451
- }
452
-
453
- installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
454
- control.meta.opera = {
455
- ...(control.meta.opera || {}),
456
- installed: true,
457
- model: "v3",
458
- stableTag: "stable",
459
- version: OPERA_VERSION,
460
- legacyStatus: "supported",
461
- contractVersion: fs.existsSync(context.paths.contractFile) ? bootstrap.CONTRACT_VERSION : null,
462
- contractReadiness: fs.existsSync(context.paths.contractFile)
463
- ? (control.meta?.opera?.contractReadiness || "verified")
464
- : "hypothesis",
465
- bootstrap: wantsReset
466
- ? bootstrap.createAwaitingBootstrapState(context)
467
- : (control.meta?.opera?.bootstrap || bootstrap.createAwaitingBootstrapState(context)),
468
- };
469
- config.saveControl(context, control);
470
- env.syncEnvironment(context, control);
471
- require("./skills").updateRegistry(context);
472
- console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
473
- console.log(t("opera.upgraded", { version: OPERA_VERSION }));
474
- }
475
-
476
- function cmdInstall(root, args) {
477
- const options = {
478
- bootstrap: true,
479
- answers: {},
480
- interactive: true,
481
- locale: null,
482
- bootstrapMode: "auto",
483
- technicalLevel: null,
484
- projectState: null,
485
- docsState: null,
486
- decisionOwnership: null,
487
- };
488
- for (let i = 0; i < (args || []).length; i += 1) {
489
- if (args[i] === "--locale" && args[i + 1]) {
490
- options.locale = args[i + 1];
491
- i += 1;
492
- } else if (args[i] === "--no-bootstrap") {
493
- options.bootstrap = false;
494
- } else if (args[i] === "--non-interactive") {
495
- options.interactive = false;
496
- } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
497
- options.bootstrapMode = args[i + 1];
498
- i += 1;
499
- } else if (args[i] === "--technical-level" && args[i + 1]) {
500
- options.technicalLevel = args[i + 1];
501
- i += 1;
502
- } else if (args[i] === "--project-state" && args[i + 1]) {
503
- options.projectState = args[i + 1];
504
- i += 1;
505
- } else if (args[i] === "--docs-state" && args[i + 1]) {
506
- options.docsState = args[i + 1];
507
- i += 1;
508
- } else if (args[i] === "--decision-ownership" && args[i + 1]) {
509
- options.decisionOwnership = args[i + 1];
510
- i += 1;
511
- }
512
- }
513
- return install(root, options);
514
- }
515
-
516
- function cmdStatus(root) { status(root); }
517
- function cmdConfigure(root, args) { configure(root, args); }
518
- function cmdUpgrade(root, args) { upgrade(root, args); }
519
-
520
- async function cmdBootstrap(root, args) {
521
- const options = {
522
- locale: null,
523
- interactive: true,
524
- answers: {},
525
- resume: false,
526
- bootstrapMode: "auto",
527
- technicalLevel: null,
528
- projectState: null,
529
- docsState: null,
530
- decisionOwnership: null,
531
- };
532
- for (let i = 0; i < (args || []).length; i += 1) {
533
- if (args[i] === "--locale" && args[i + 1]) {
534
- options.locale = args[i + 1];
535
- i += 1;
536
- } else if (args[i] === "--non-interactive") {
537
- options.interactive = false;
538
- } else if (args[i] === "--resume") {
539
- options.resume = true;
540
- } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
541
- options.bootstrapMode = args[i + 1];
542
- i += 1;
543
- } else if (args[i] === "--technical-level" && args[i + 1]) {
544
- options.technicalLevel = args[i + 1];
545
- i += 1;
546
- } else if (args[i] === "--project-state" && args[i + 1]) {
547
- options.projectState = args[i + 1];
548
- i += 1;
549
- } else if (args[i] === "--docs-state" && args[i + 1]) {
550
- options.docsState = args[i + 1];
551
- i += 1;
552
- } else if (args[i] === "--decision-ownership" && args[i + 1]) {
553
- options.decisionOwnership = args[i + 1];
554
- i += 1;
555
- }
556
- }
557
- return runBootstrap(root, options);
558
- }
559
-
560
- function cmdHandoff(root, args) {
561
- const context = config.ensureContext(root);
562
- const control = config.loadControl(context);
563
- const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
564
- if (!state) {
565
- throw new Error("OPERA bootstrap is not initialized.");
566
- }
567
- const files = bootstrap.bootstrapFilePaths(context);
568
- const printMode = (args || []).includes("--print");
569
- const jsonMode = (args || []).includes("--json");
570
- if (jsonMode) {
571
- const payload = readText(files.json);
572
- process.stdout.write(payload || "{}\n");
573
- return;
574
- }
575
- if (printMode) {
576
- process.stdout.write(readText(files.markdown) || "");
577
- return;
578
- }
579
- console.log(`Bootstrap: ${state.status}`);
580
- console.log(`Mode: ${state.mode}`);
581
- console.log(`Markdown handoff: ${state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
582
- console.log(`JSON handoff: ${state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json}`);
583
- if (state.reviewFiles?.openQuestions) {
584
- console.log(`Open questions: ${state.reviewFiles.openQuestions}`);
585
- }
586
- }
587
-
588
- module.exports = {
589
- installStructure,
590
- install,
591
- runBootstrap,
421
+ config.saveControl(context, control);
422
+ if (config.isOperaInstalled(control)) {
423
+ installStructure(context, control, nextLocale, { rewriteLocalizedTemplates: true });
424
+ env.syncEnvironment(context, control);
425
+ const ops = require("./control");
426
+ ops.syncDocs(context, control);
427
+ }
428
+ console.log(t("opera.configure.updated"));
429
+ }
430
+
431
+ function upgrade(root, args = []) {
432
+ const context = config.ensureContext(root);
433
+ const control = config.loadControl(context);
434
+ const locale = config.getLocale(control);
435
+ setLocale(locale);
436
+ const wantsStable = (args || []).includes("--stable");
437
+ const wantsReset = (args || []).includes("--reset");
438
+
439
+ if (!config.isOperaInstalled(control)) {
440
+ console.log(t("opera.notInstalled"));
441
+ console.log(t("opera.upgrade.runInstallFirst"));
442
+ return;
443
+ }
444
+
445
+ if (!wantsStable) {
446
+ console.log(t("opera.upgrade.usage"));
447
+ return;
448
+ }
449
+
450
+ const legacy = bootstrap.detectLegacyBootstrap(context, control);
451
+ const isLegacyUnsupported = legacy?.status === "legacy_unsupported";
452
+ if (isLegacyUnsupported && !wantsReset) {
453
+ control.meta.opera = control.meta.opera || {};
454
+ control.meta.opera.legacyStatus = "legacy_unsupported";
455
+ config.saveControl(context, control);
456
+ console.log(t("opera.upgrade.legacyUnsupported"));
457
+ return;
458
+ }
459
+
460
+ const backup = backupManagedArtifacts(context);
461
+ if (wantsReset) {
462
+ removePath(context.paths.agentHubDir);
463
+ removePath(context.paths.skillsDir);
464
+ removePath(context.paths.genesisFile);
465
+ removePath(context.paths.contractFile);
466
+ removePath(context.paths.autonomyPolicyFile);
467
+ removePath(context.paths.bootstrapDir);
468
+ }
469
+
470
+ installStructure(context, control, locale, { rewriteLocalizedTemplates: true });
471
+ control.meta.opera = {
472
+ ...(control.meta.opera || {}),
473
+ installed: true,
474
+ model: "v3",
475
+ stableTag: "stable",
476
+ version: OPERA_VERSION,
477
+ legacyStatus: "supported",
478
+ contractVersion: fs.existsSync(context.paths.contractFile) ? bootstrap.CONTRACT_VERSION : null,
479
+ contractReadiness: fs.existsSync(context.paths.contractFile)
480
+ ? (control.meta?.opera?.contractReadiness || "verified")
481
+ : "hypothesis",
482
+ bootstrap: wantsReset
483
+ ? bootstrap.createAwaitingBootstrapState(context)
484
+ : (control.meta?.opera?.bootstrap || bootstrap.createAwaitingBootstrapState(context)),
485
+ };
486
+ config.saveControl(context, control);
487
+ env.syncEnvironment(context, control);
488
+ require("./skills").updateRegistry(context);
489
+ console.log(t("opera.upgrade.backup", { path: path.relative(context.workspaceRoot, backup.backupRoot) }));
490
+ console.log(t("opera.upgraded", { version: OPERA_VERSION }));
491
+ }
492
+
493
+ function cmdInstall(root, args) {
494
+ const options = {
495
+ bootstrap: true,
496
+ answers: {},
497
+ interactive: true,
498
+ locale: null,
499
+ bootstrapMode: "auto",
500
+ technicalLevel: null,
501
+ projectState: null,
502
+ docsState: null,
503
+ decisionOwnership: null,
504
+ };
505
+ for (let i = 0; i < (args || []).length; i += 1) {
506
+ if (args[i] === "--locale" && args[i + 1]) {
507
+ options.locale = args[i + 1];
508
+ i += 1;
509
+ } else if (args[i] === "--no-bootstrap") {
510
+ options.bootstrap = false;
511
+ } else if (args[i] === "--non-interactive") {
512
+ options.interactive = false;
513
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
514
+ options.bootstrapMode = args[i + 1];
515
+ i += 1;
516
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
517
+ options.technicalLevel = args[i + 1];
518
+ i += 1;
519
+ } else if (args[i] === "--project-state" && args[i + 1]) {
520
+ options.projectState = args[i + 1];
521
+ i += 1;
522
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
523
+ options.docsState = args[i + 1];
524
+ i += 1;
525
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
526
+ options.decisionOwnership = args[i + 1];
527
+ i += 1;
528
+ }
529
+ }
530
+ return install(root, options);
531
+ }
532
+
533
+ function cmdStatus(root) { status(root); }
534
+ function cmdConfigure(root, args) { configure(root, args); }
535
+ function cmdUpgrade(root, args) { upgrade(root, args); }
536
+
537
+ async function cmdBootstrap(root, args) {
538
+ const options = {
539
+ locale: null,
540
+ interactive: true,
541
+ answers: {},
542
+ resume: false,
543
+ bootstrapMode: "auto",
544
+ technicalLevel: null,
545
+ projectState: null,
546
+ docsState: null,
547
+ decisionOwnership: null,
548
+ };
549
+ for (let i = 0; i < (args || []).length; i += 1) {
550
+ if (args[i] === "--locale" && args[i + 1]) {
551
+ options.locale = args[i + 1];
552
+ i += 1;
553
+ } else if (args[i] === "--non-interactive") {
554
+ options.interactive = false;
555
+ } else if (args[i] === "--resume") {
556
+ options.resume = true;
557
+ } else if (args[i] === "--bootstrap-mode" && args[i + 1]) {
558
+ options.bootstrapMode = args[i + 1];
559
+ i += 1;
560
+ } else if (args[i] === "--technical-level" && args[i + 1]) {
561
+ options.technicalLevel = args[i + 1];
562
+ i += 1;
563
+ } else if (args[i] === "--project-state" && args[i + 1]) {
564
+ options.projectState = args[i + 1];
565
+ i += 1;
566
+ } else if (args[i] === "--docs-state" && args[i + 1]) {
567
+ options.docsState = args[i + 1];
568
+ i += 1;
569
+ } else if (args[i] === "--decision-ownership" && args[i + 1]) {
570
+ options.decisionOwnership = args[i + 1];
571
+ i += 1;
572
+ }
573
+ }
574
+ return runBootstrap(root, options);
575
+ }
576
+
577
+ function cmdHandoff(root, args) {
578
+ const context = config.ensureContext(root);
579
+ const control = config.loadControl(context);
580
+ const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
581
+ if (!state) {
582
+ throw new Error("OPERA bootstrap is not initialized.");
583
+ }
584
+ const files = bootstrap.bootstrapFilePaths(context);
585
+ const printMode = (args || []).includes("--print");
586
+ const jsonMode = (args || []).includes("--json");
587
+ if (jsonMode) {
588
+ const payload = readText(files.json);
589
+ process.stdout.write(payload || "{}\n");
590
+ return;
591
+ }
592
+ if (printMode) {
593
+ process.stdout.write(readText(files.markdown) || "");
594
+ return;
595
+ }
596
+ console.log(`Bootstrap: ${state.status}`);
597
+ console.log(`Mode: ${state.mode}`);
598
+ console.log(`Markdown handoff: ${state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown}`);
599
+ console.log(`JSON handoff: ${state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json}`);
600
+ if (state.reviewFiles?.openQuestions) {
601
+ console.log(`Open questions: ${state.reviewFiles.openQuestions}`);
602
+ }
603
+ }
604
+
605
+ module.exports = {
606
+ installStructure,
607
+ install,
608
+ runBootstrap,
592
609
  status,
593
610
  configure,
594
611
  upgrade,
595
612
  cmdInstall,
596
613
  cmdStatus,
597
- cmdConfigure,
598
- cmdUpgrade,
599
- cmdBootstrap,
600
- cmdHandoff,
601
- };
614
+ cmdConfigure,
615
+ cmdUpgrade,
616
+ cmdBootstrap,
617
+ cmdHandoff,
618
+ };