trackops 2.0.5 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/opera.js CHANGED
@@ -10,6 +10,7 @@ const { promptForLocale, maybePromptForLocale, resolveLocale } = require("./loca
10
10
  const { resolveLocalizedFile, resolveLocalizedDir, resolveSkillFile } = require("./resources");
11
11
  const bootstrap = require("./opera-bootstrap");
12
12
  const runtimeState = require("./runtime-state");
13
+ const fmt = require("./cli-format");
13
14
 
14
15
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
15
16
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
@@ -20,9 +21,41 @@ function nowIso() {
20
21
  return new Date().toISOString();
21
22
  }
22
23
 
23
- function formatLocaleSource(source) {
24
- return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
25
- }
24
+ function formatLocaleSource(source) {
25
+ return t(`locale.source.${String(source || "").trim()}`) || source || t("locale.none");
26
+ }
27
+
28
+ function formatBootstrapStatus(status) {
29
+ return t(`bootstrap.status.${String(status || "").trim()}`) || status || t("locale.none");
30
+ }
31
+
32
+ function formatBootstrapMode(mode) {
33
+ return t(`bootstrap.mode.${String(mode || "").trim()}`) || mode || t("locale.none");
34
+ }
35
+
36
+ function formatBootstrapReason(reason) {
37
+ return t(`bootstrap.reason.${String(reason || "").trim()}`) || reason || t("locale.none");
38
+ }
39
+
40
+ function formatDecisionOwnership(value) {
41
+ return t(`bootstrap.ownership.${String(value || "").trim()}`) || value || t("locale.none");
42
+ }
43
+
44
+ function formatContractReadiness(value) {
45
+ return t(`bootstrap.readiness.${String(value || "").trim()}`) || value || t("locale.none");
46
+ }
47
+
48
+ function formatLegacyStatus(value) {
49
+ return t(`bootstrap.legacy.${String(value || "").trim()}`) || value || t("locale.none");
50
+ }
51
+
52
+ function formatMissingFields(fields) {
53
+ return (fields || []).map((field) => t(`bootstrap.field.${field}`) || field).join(", ");
54
+ }
55
+
56
+ function relativePathExists(context, relativePath) {
57
+ return Boolean(relativePath) && fs.existsSync(path.join(context.workspaceRoot, relativePath));
58
+ }
26
59
 
27
60
  function readText(filePath) {
28
61
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
@@ -88,7 +121,10 @@ function resolveOperaLocale(control, options = {}) {
88
121
 
89
122
  function seedAuxiliarySkill(skillName, locale, skillsDir, options = {}) {
90
123
  const templateDir = path.join(SKILLS_TEMPLATES_DIR, skillName);
91
- if (!fs.existsSync(templateDir)) return;
124
+ if (!fs.existsSync(templateDir)) {
125
+ process.stderr.write(`[opera] Warning: skill template not found: ${skillName} (${templateDir})\n`);
126
+ return;
127
+ }
92
128
  const targetDir = path.join(skillsDir, skillName);
93
129
  seedDirRecursive(templateDir, targetDir, {
94
130
  overwrite: options.rewriteLocalizedTemplates === true,
@@ -222,22 +258,36 @@ async function install(root, options = {}) {
222
258
  config.saveControl(context, control);
223
259
  env.syncEnvironment(context, control);
224
260
 
261
+ fmt.blank();
225
262
  if (!alreadyInstalled) {
226
- console.log(t("opera.installed", { version: OPERA_VERSION }));
263
+ fmt.success(t("opera.installed", { version: OPERA_VERSION }));
227
264
  } else {
228
- console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
265
+ fmt.info(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) || OPERA_VERSION }));
229
266
  }
230
267
 
231
268
  const skills = require("./skills");
232
269
  for (const skillName of ["commiter", "changelog-updater"]) {
233
270
  try {
234
271
  skills.installSkill(context, skillName, { locale });
235
- } catch (_error) {
236
- // ignore
272
+ } catch (error) {
273
+ fmt.warn(t("skill.installError", { name: skillName, error: error.message }));
237
274
  }
238
275
  }
239
276
  skills.updateRegistry(context);
240
277
 
278
+ if (options.bootstrap !== false && options.interactive !== false) {
279
+ const readline = require("readline");
280
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
281
+ const answer = await new Promise((resolve) => {
282
+ rl.question(` ${t("opera.install.bootstrapConfirm")} `, (ans) => { rl.close(); resolve(ans); });
283
+ });
284
+ const normalizedAnswer = String(answer || "").trim().toLowerCase();
285
+ if (!["y", "yes", "s", "si"].includes(normalizedAnswer)) {
286
+ fmt.info(t("opera.install.bootstrapSkipped"));
287
+ fmt.blank();
288
+ return;
289
+ }
290
+ }
241
291
  if (options.bootstrap !== false) {
242
292
  await runBootstrap(context, {
243
293
  locale,
@@ -302,37 +352,85 @@ async function runBootstrap(root, options = {}) {
302
352
  if ((options.resume || options.forceResume) && control.meta?.opera?.bootstrap) {
303
353
  const resumed = bootstrap.resumeBootstrap(context, control);
304
354
  if (resumed.resumed) {
305
- const updatedControl = bootstrap.applyBootstrap(context, control, resumed.profile);
355
+ const profile = resumed.profile;
356
+ const updatedControl = bootstrap.applyBootstrap(context, control, profile);
306
357
  env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
307
358
  const ops = require("./control");
308
359
  ops.syncDocs(context, updatedControl);
309
360
  ops.refreshRepoRuntime(context, { quiet: true });
310
- console.log(t("bootstrap.completed"));
311
- return resumed.profile;
361
+ fmt.blank();
362
+
363
+ if (profile.status === "completed") {
364
+ fmt.success(t("bootstrap.completed"));
365
+ fmt.blank();
366
+ fmt.step(t("bootstrap.next.label"));
367
+ fmt.hint(t("bootstrap.next.directCompleted"));
368
+ } else if (profile.status === "blocked") {
369
+ fmt.warn(t("bootstrap.resumeBlocked"));
370
+ for (const field of profile.missingFields || []) {
371
+ fmt.bullet(t(`bootstrap.field.${field}`) || field);
372
+ }
373
+ fmt.blank();
374
+ fmt.hint(t("bootstrap.next.directPending"));
375
+ } else if (profile.status === "needs_review") {
376
+ fmt.info(t("bootstrap.resumeNeedsReview"));
377
+ for (const field of profile.missingFields || []) {
378
+ fmt.bullet(t(`bootstrap.field.${field}`) || field);
379
+ }
380
+ fmt.blank();
381
+ fmt.hint(t("bootstrap.next.directPending"));
382
+ }
383
+
384
+ fmt.blank();
385
+ return profile;
312
386
  }
313
- console.log(t("bootstrap.awaitingAgent"));
387
+ fmt.blank();
388
+ if (resumed.reason === "missing_agent_artifacts") {
389
+ fmt.warn(t("bootstrap.resumeAwaitingArtifacts"));
390
+ } else if (resumed.reason === "empty_intake_and_spec") {
391
+ fmt.warn(t("bootstrap.resumeEmptyArtifacts"));
392
+ } else {
393
+ fmt.info(t("bootstrap.awaitingAgent"));
394
+ }
395
+ fmt.hint(t("bootstrap.next.handoff"));
396
+ fmt.blank();
314
397
  return control.meta.opera.bootstrap;
315
398
  }
316
399
 
317
- const profile = await bootstrap.collectBootstrapProfile(context, control, options);
400
+ const profile = await bootstrap.collectBootstrapProfile(context, control, options);
401
+ if (profile.mode && profile.routeReason) {
402
+ fmt.blank();
403
+ fmt.info(t("bootstrap.routingMode", {
404
+ mode: formatBootstrapMode(profile.mode),
405
+ reason: formatBootstrapReason(profile.routeReason),
406
+ }));
407
+ }
318
408
  const updatedControl = bootstrap.applyBootstrap(context, control, profile);
319
409
  env.syncEnvironment(context, updatedControl, { requiredKeys: env.inferRequiredKeys(updatedControl, context) });
320
410
  const ops = require("./control");
321
411
  ops.syncDocs(context, updatedControl);
322
412
  ops.refreshRepoRuntime(context, { quiet: true });
323
413
 
414
+ fmt.header(t("bootstrap.resultHeader"));
415
+
324
416
  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"));
417
+ fmt.info(t("bootstrap.awaitingAgent"));
418
+ fmt.blank();
419
+ fmt.step(t("bootstrap.handoffFile"), profile.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown);
420
+ fmt.blank();
421
+ fmt.step(t("bootstrap.next.label"));
422
+ fmt.hint(t("bootstrap.next.handoff"));
328
423
  } else {
329
- console.log(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
330
- console.log(
424
+ fmt.success(profile.status === "completed" ? t("bootstrap.completed") : t("bootstrap.pending"));
425
+ fmt.blank();
426
+ fmt.step(t("bootstrap.next.label"));
427
+ fmt.hint(
331
428
  profile.status === "completed"
332
429
  ? t("bootstrap.next.directCompleted")
333
430
  : t("bootstrap.next.directPending"),
334
431
  );
335
432
  }
433
+ fmt.blank();
336
434
  return profile;
337
435
  }
338
436
 
@@ -346,39 +444,59 @@ function status(root) {
346
444
  return;
347
445
  }
348
446
 
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
- }
447
+ const opera = control.meta.opera;
448
+ const bootstrapState = opera.bootstrap || bootstrap.detectLegacyBootstrap(context, control);
449
+ const localeDoctor = runtimeState.doctorLocale(control.meta?.locale || null);
450
+ console.log(t("opera.status.version", { version: opera.version }));
451
+ console.log(t("opera.status.installed", { value: opera.installedAt }));
452
+ console.log(t("opera.status.skills", { value: (opera.skills || []).join(", ") || t("locale.none") }));
453
+ console.log(t("opera.status.locale", { locale: config.getLocale(control), source: formatLocaleSource(localeDoctor.source) }));
454
+ console.log(t("opera.status.legacy", { value: formatLegacyStatus(opera.legacyStatus || bootstrapState?.status || "supported") }));
455
+ console.log(t("opera.status.contractVersion", { value: opera.contractVersion || t("locale.none") }));
456
+ console.log(t("opera.status.contractReadiness", { value: formatContractReadiness(opera.contractReadiness || "hypothesis") }));
457
+
458
+ if (bootstrapState) {
459
+ console.log(t("opera.status.bootstrap", { value: formatBootstrapStatus(bootstrapState.status) }));
460
+ if (bootstrapState.mode) {
461
+ console.log(t("opera.status.mode", { value: formatBootstrapMode(bootstrapState.mode) }));
462
+ }
463
+ if (bootstrapState.routeReason) {
464
+ console.log(t("opera.status.route", { value: formatBootstrapReason(bootstrapState.routeReason) }));
465
+ }
466
+ if (bootstrapState.decisionOwnership) {
467
+ console.log(t("opera.status.ownership", { value: formatDecisionOwnership(bootstrapState.decisionOwnership) }));
468
+ }
469
+ if ((bootstrapState.missingFields || []).length) {
470
+ console.log(t("opera.status.missing", { value: formatMissingFields(bootstrapState.missingFields) }));
471
+ if (bootstrapState.mode === "agent_handoff") {
472
+ console.log(t("opera.status.awaitingAgentExplanation"));
473
+ console.log(t("opera.status.awaitingAgentAction"));
474
+ } else {
475
+ console.log(t("opera.status.directExplanation"));
476
+ console.log(t("opera.status.directAction", {
477
+ intake: bootstrapState.intakeFiles?.json || bootstrap.bootstrapRelativePaths(context).intakeJson,
478
+ spec: bootstrapState.intakeFiles?.specDossier || bootstrap.bootstrapRelativePaths(context).specDossier,
479
+ }));
480
+ }
481
+ console.log(t("opera.status.contractNotGenerated"));
482
+ console.log(t("opera.status.resume"));
483
+ }
484
+ if (bootstrapState.mode === "agent_handoff" && relativePathExists(context, bootstrapState.handoffFiles?.markdown)) {
485
+ console.log(t("opera.status.handoff", { value: bootstrapState.handoffFiles.markdown }));
486
+ }
487
+ if (relativePathExists(context, bootstrapState.intakeFiles?.json)) {
488
+ console.log(t("opera.status.intake", { value: bootstrapState.intakeFiles.json }));
489
+ }
490
+ if (relativePathExists(context, bootstrapState.intakeFiles?.specDossier)) {
491
+ console.log(t("opera.status.specDossier", { value: bootstrapState.intakeFiles.specDossier }));
492
+ }
493
+ if (relativePathExists(context, bootstrapState.reviewFiles?.openQuestions)) {
494
+ console.log(t("opera.status.openQuestions", { value: bootstrapState.reviewFiles.openQuestions }));
495
+ }
496
+ if (relativePathExists(context, bootstrapState.reviewFiles?.qualityReport)) {
497
+ console.log(t("opera.status.qualityReport", { value: bootstrapState.reviewFiles.qualityReport }));
498
+ }
499
+ }
382
500
 
383
501
  const checks = [
384
502
  [context.layout === "split" ? "ops/.agent/hub/agent.md" : ".agent/hub/agent.md", fs.existsSync(path.join(context.paths.agentHubDir, "agent.md"))],
@@ -389,11 +507,11 @@ function status(root) {
389
507
  [context.layout === "split" ? "ops/policy/autonomy.json" : "policy/autonomy.json", fs.existsSync(context.paths.autonomyPolicyFile)],
390
508
  ];
391
509
 
392
- console.log(t("opera.status.structure"));
393
- for (const [file, exists] of checks) {
394
- console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
395
- }
396
- }
510
+ console.log(t("opera.status.structure"));
511
+ for (const [file, exists] of checks) {
512
+ console.log(` ${fmt.boolToken(exists)} ${file}`);
513
+ }
514
+ }
397
515
 
398
516
  function configure(root, args) {
399
517
  const context = config.ensureContext(root);
@@ -574,33 +692,77 @@ async function cmdBootstrap(root, args) {
574
692
  return runBootstrap(root, options);
575
693
  }
576
694
 
577
- function cmdHandoff(root, args) {
695
+ function cmdHandoff(root, args) {
578
696
  const context = config.ensureContext(root);
579
697
  const control = config.loadControl(context);
580
698
  const state = bootstrap.getBootstrapState(control, context) || bootstrap.detectLegacyBootstrap(context, control);
581
699
  if (!state) {
582
700
  throw new Error("OPERA bootstrap is not initialized.");
583
701
  }
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
- }
702
+ const files = bootstrap.bootstrapFilePaths(context);
703
+ const printMode = (args || []).includes("--print");
704
+ const jsonMode = (args || []).includes("--json");
705
+ if (state.mode !== "agent_handoff") {
706
+ const payload = {
707
+ mode: state.mode,
708
+ status: state.status,
709
+ files: {
710
+ intakeJson: state.intakeFiles?.json || bootstrap.bootstrapRelativePaths(context).intakeJson,
711
+ specDossier: state.intakeFiles?.specDossier || bootstrap.bootstrapRelativePaths(context).specDossier,
712
+ openQuestions: state.reviewFiles?.openQuestions || bootstrap.bootstrapRelativePaths(context).openQuestions,
713
+ qualityReport: state.reviewFiles?.qualityReport || bootstrap.bootstrapRelativePaths(context).qualityReport,
714
+ },
715
+ nextStep: t("opera.handoff.directNext"),
716
+ };
717
+ if (jsonMode) {
718
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
719
+ return;
720
+ }
721
+ if (printMode) {
722
+ process.stdout.write([
723
+ `# ${t("opera.handoff.directTitle")}`,
724
+ "",
725
+ `- ${t("opera.handoff.directStatus")}: ${formatBootstrapStatus(state.status)}`,
726
+ `- ${t("opera.handoff.directIntake")}: ${payload.files.intakeJson}`,
727
+ `- ${t("opera.handoff.directSpec")}: ${payload.files.specDossier}`,
728
+ `- ${t("opera.handoff.directQuestions")}: ${payload.files.openQuestions}`,
729
+ `- ${t("opera.handoff.directQuality")}: ${payload.files.qualityReport}`,
730
+ "",
731
+ t("opera.handoff.directNext"),
732
+ "",
733
+ ].join("\n"));
734
+ return;
735
+ }
736
+ console.log(t("opera.handoff.directSummary", { status: formatBootstrapStatus(state.status) }));
737
+ console.log(t("opera.status.intake", { value: payload.files.intakeJson }));
738
+ console.log(t("opera.status.specDossier", { value: payload.files.specDossier }));
739
+ if (relativePathExists(context, payload.files.openQuestions)) {
740
+ console.log(t("opera.status.openQuestions", { value: payload.files.openQuestions }));
741
+ }
742
+ if (relativePathExists(context, payload.files.qualityReport)) {
743
+ console.log(t("opera.status.qualityReport", { value: payload.files.qualityReport }));
744
+ }
745
+ console.log(t("opera.status.resume"));
746
+ console.log(t("opera.handoff.directNext"));
747
+ return;
748
+ }
749
+ if (jsonMode) {
750
+ const payload = readText(files.json);
751
+ process.stdout.write(payload || "{}\n");
752
+ return;
753
+ }
592
754
  if (printMode) {
593
755
  process.stdout.write(readText(files.markdown) || "");
594
756
  return;
595
757
  }
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
- }
758
+ console.log(t("opera.handoff.summary", { status: formatBootstrapStatus(state.status) }));
759
+ console.log(t("opera.handoff.mode", { mode: formatBootstrapMode(state.mode) }));
760
+ console.log(t("opera.handoff.markdown", { value: state.handoffFiles?.markdown || bootstrap.bootstrapRelativePaths(context).markdown }));
761
+ console.log(t("opera.handoff.json", { value: state.handoffFiles?.json || bootstrap.bootstrapRelativePaths(context).json }));
762
+ if (relativePathExists(context, state.reviewFiles?.openQuestions)) {
763
+ console.log(t("opera.status.openQuestions", { value: state.reviewFiles.openQuestions }));
764
+ }
765
+ }
604
766
 
605
767
  module.exports = {
606
768
  installStructure,
package/lib/skills.js CHANGED
@@ -6,6 +6,7 @@ const path = require("path");
6
6
  const config = require("./config");
7
7
  const { t, setLocale } = require("./i18n");
8
8
  const { resolveSkillFile } = require("./resources");
9
+ const fmt = require("./cli-format");
9
10
 
10
11
  const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
11
12
  const INSTALLED_SKILL_PRIORITY = [
@@ -102,11 +103,14 @@ function updateRegistry(root) {
102
103
  }
103
104
  }
104
105
  const skills = installedSkills(root);
105
- const header = locale === "en" ? "# Skills Registry" : "# Skills Registry";
106
- const empty = locale === "en"
107
- ? "_No skills installed yet. Use `trackops skill install <name>` to add one._"
108
- : "_Sin skills instaladas. Usa `trackops skill install <nombre>` para añadir una._";
109
- const lines = [header, "", "| Skill | Version | Description |", "|-------|---------|-------------|"];
106
+ const header = locale === "en" ? "# Skills Registry" : "# Registro de skills";
107
+ const empty = locale === "en"
108
+ ? "_No skills installed yet. Use `trackops skill install <name>` to add one._"
109
+ : "_Todavia no hay skills instaladas. Usa `trackops skill install <nombre>` para añadir una._";
110
+ const tableHeader = locale === "en"
111
+ ? "| Skill | Version | Description |"
112
+ : "| Skill | Version | Descripcion |";
113
+ const lines = [header, "", tableHeader, "|-------|---------|-------------|"];
110
114
  for (const s of skills) {
111
115
  lines.push(`| ${s.name} | ${s.version} | ${s.description} |`);
112
116
  }
@@ -121,7 +125,11 @@ function updateRegistry(root) {
121
125
  control.meta.opera.skills = skills.map((s) => s.name);
122
126
  config.saveControl(context, control);
123
127
  }
124
- } catch (_e) { /* ignore */ }
128
+ } catch (error) {
129
+ if (process.env.TRACKOPS_DEBUG) {
130
+ process.stderr.write(`[skills] Registry update error: ${error.message}\n`);
131
+ }
132
+ }
125
133
  }
126
134
  }
127
135
 
@@ -148,7 +156,7 @@ function installSkill(root, skillName) {
148
156
  fs.copyFileSync(localizedSkill, path.join(targetDir, "SKILL.md"));
149
157
  }
150
158
  updateRegistry(context);
151
- console.log(t("skill.installed", { name: skillName }));
159
+ fmt.success(t("skill.installed", { name: skillName }));
152
160
  }
153
161
 
154
162
  function removeSkill(root, skillName) {
@@ -164,37 +172,37 @@ function removeSkill(root, skillName) {
164
172
 
165
173
  fs.rmSync(targetDir, { recursive: true, force: true });
166
174
  updateRegistry(context);
167
- console.log(t("skill.removed", { name: skillName }));
175
+ fmt.success(t("skill.removed", { name: skillName }));
168
176
  }
169
177
 
170
178
  /* ── CLI commands ── */
171
179
 
172
- function cmdInstall(root, skillName) {
173
- if (!skillName) { console.error("Skill name required."); process.exit(1); }
174
- installSkill(root, skillName);
175
- }
176
-
177
- function cmdList(root) {
178
- const skills = installedSkills(root);
179
- if (!skills.length) { console.log("No skills installed."); return; }
180
- console.log(t("skill.listTitle"));
181
- for (const s of skills) {
182
- console.log(` ${s.name} (v${s.version}) — ${s.description}`);
183
- }
184
- }
185
-
186
- function cmdRemove(root, skillName) {
187
- if (!skillName) { console.error("Skill name required."); process.exit(1); }
188
- removeSkill(root, skillName);
189
- }
190
-
191
- function cmdCatalog() {
192
- const skills = catalogSkills();
193
- if (!skills.length) { console.log("No skills available in catalog."); return; }
194
- console.log(t("skill.catalogTitle"));
195
- for (const s of skills) {
196
- console.log(` ${s.name} (v${s.version}) — ${s.description}`);
197
- }
198
- }
180
+ function cmdInstall(root, skillName) {
181
+ if (!skillName) { console.error(t("skill.nameRequired")); process.exit(1); }
182
+ installSkill(root, skillName);
183
+ }
184
+
185
+ function cmdList(root) {
186
+ const skills = installedSkills(root);
187
+ if (!skills.length) { console.log(t("skill.noneInstalled")); return; }
188
+ console.log(t("skill.listTitle"));
189
+ for (const s of skills) {
190
+ console.log(` ${s.name} (v${s.version}) — ${s.description}`);
191
+ }
192
+ }
193
+
194
+ function cmdRemove(root, skillName) {
195
+ if (!skillName) { console.error(t("skill.nameRequired")); process.exit(1); }
196
+ removeSkill(root, skillName);
197
+ }
198
+
199
+ function cmdCatalog() {
200
+ const skills = catalogSkills();
201
+ if (!skills.length) { console.log(t("skill.noneCatalog")); return; }
202
+ console.log(t("skill.catalogTitle"));
203
+ for (const s of skills) {
204
+ console.log(` ${s.name} (v${s.version}) — ${s.description}`);
205
+ }
206
+ }
199
207
 
200
208
  module.exports = { installSkill, removeSkill, installedSkills, catalogSkills, updateRegistry, cmdInstall, cmdList, cmdRemove, cmdCatalog };
package/lib/workspace.js CHANGED
@@ -4,9 +4,10 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { spawnSync } = require("child_process");
6
6
 
7
- const config = require("./config");
8
- const registry = require("./registry");
9
- const env = require("./env");
7
+ const config = require("./config");
8
+ const registry = require("./registry");
9
+ const env = require("./env");
10
+ const { t, setLocale } = require("./i18n");
10
11
 
11
12
  const OPS_ARTIFACTS = [
12
13
  "project_control.json",
@@ -224,29 +225,39 @@ function migrateWorkspace(rootDir, options = {}) {
224
225
  return { root, backupBranch, context };
225
226
  }
226
227
 
227
- function status(contextOrRoot) {
228
- const context = config.ensureContext(contextOrRoot);
229
- console.log("Workspace:");
230
- console.log(` Layout: ${context.layout}`);
231
- console.log(` Root: ${context.workspaceRoot}`);
232
- console.log(` App: ${context.appRoot}`);
233
- console.log(` Ops: ${context.opsRoot}`);
234
- if (context.manifestFile) {
235
- console.log(` Manifest: ${context.manifestFile}`);
236
- }
237
- console.log(` Control: ${context.controlFile}`);
238
- }
228
+ function status(contextOrRoot) {
229
+ const context = config.ensureContext(contextOrRoot);
230
+ try {
231
+ setLocale(config.getLocale(config.loadControl(context)));
232
+ } catch (_error) {
233
+ setLocale("es");
234
+ }
235
+ console.log(t("workspace.status.title"));
236
+ console.log(t("workspace.status.layout", { value: context.layout }));
237
+ console.log(t("workspace.status.root", { path: context.workspaceRoot }));
238
+ console.log(t("workspace.status.app", { path: context.appRoot }));
239
+ console.log(t("workspace.status.ops", { path: context.opsRoot }));
240
+ if (context.manifestFile) {
241
+ console.log(t("workspace.status.manifest", { path: context.manifestFile }));
242
+ }
243
+ console.log(t("workspace.status.control", { path: context.controlFile }));
244
+ }
239
245
 
240
246
  function cmdStatus(root) {
241
247
  status(root);
242
248
  }
243
249
 
244
- function cmdMigrate(root, args = []) {
245
- const allowDirty = args.includes("--allow-dirty");
246
- const result = migrateWorkspace(root, { allowDirty });
247
- console.log(`Workspace migrated: ${result.root}`);
248
- console.log(`Backup branch: ${result.backupBranch}`);
249
- }
250
+ function cmdMigrate(root, args = []) {
251
+ const allowDirty = args.includes("--allow-dirty");
252
+ const result = migrateWorkspace(root, { allowDirty });
253
+ try {
254
+ setLocale(config.getLocale(config.loadControl(result.context)));
255
+ } catch (_error) {
256
+ setLocale("es");
257
+ }
258
+ console.log(t("workspace.migrate.updated", { path: result.root }));
259
+ console.log(t("workspace.migrate.backup", { branch: result.backupBranch }));
260
+ }
250
261
 
251
262
  module.exports = {
252
263
  OPS_ARTIFACTS,