trackops 1.0.1 → 2.0.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 (83) hide show
  1. package/README.md +292 -272
  2. package/bin/trackops.js +108 -50
  3. package/lib/config.js +267 -38
  4. package/lib/control.js +534 -480
  5. package/lib/env.js +244 -0
  6. package/lib/i18n.js +61 -53
  7. package/lib/init.js +170 -47
  8. package/lib/locale.js +63 -0
  9. package/lib/opera-bootstrap.js +1075 -0
  10. package/lib/opera.js +524 -125
  11. package/lib/preferences.js +74 -0
  12. package/lib/registry.js +27 -13
  13. package/lib/release.js +56 -0
  14. package/lib/resources.js +42 -0
  15. package/lib/runtime-state.js +144 -0
  16. package/lib/server.js +1004 -521
  17. package/lib/skills.js +148 -124
  18. package/lib/workspace.js +260 -0
  19. package/locales/en.json +418 -132
  20. package/locales/es.json +418 -132
  21. package/package.json +8 -9
  22. package/scripts/postinstall-locale.js +21 -0
  23. package/scripts/skills-marketplace-smoke.js +124 -0
  24. package/scripts/smoke-tests.js +570 -0
  25. package/scripts/sync-skill-version.js +21 -0
  26. package/scripts/validate-skill.js +89 -0
  27. package/skills/trackops/SKILL.md +89 -0
  28. package/skills/trackops/agents/openai.yaml +3 -0
  29. package/skills/trackops/references/activation.md +73 -0
  30. package/skills/trackops/references/troubleshooting.md +49 -0
  31. package/skills/trackops/references/workflow.md +26 -0
  32. package/skills/trackops/scripts/bootstrap-trackops.js +203 -0
  33. package/skills/trackops/skill.json +29 -0
  34. package/templates/opera/agent.md +10 -9
  35. package/templates/opera/architecture/dependency-graph.md +24 -0
  36. package/templates/opera/architecture/runtime-automation.md +24 -0
  37. package/templates/opera/architecture/runtime-operations.md +34 -0
  38. package/templates/opera/en/agent.md +27 -0
  39. package/templates/opera/en/architecture/dependency-graph.md +24 -0
  40. package/templates/opera/en/architecture/runtime-automation.md +24 -0
  41. package/templates/opera/en/architecture/runtime-operations.md +34 -0
  42. package/templates/opera/en/genesis.md +79 -0
  43. package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
  44. package/templates/opera/en/references/opera-cycle.md +62 -0
  45. package/templates/opera/en/registry.md +28 -0
  46. package/templates/opera/en/reviews/delivery-audit.md +18 -0
  47. package/templates/opera/en/reviews/integration-audit.md +18 -0
  48. package/templates/opera/en/router.md +49 -0
  49. package/templates/opera/genesis.md +79 -94
  50. package/templates/opera/reviews/delivery-audit.md +18 -0
  51. package/templates/opera/reviews/integration-audit.md +18 -0
  52. package/templates/opera/router.md +15 -5
  53. package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
  54. package/templates/skills/commiter/locales/en/SKILL.md +11 -0
  55. package/templates/skills/opera-contract-auditor/SKILL.md +38 -0
  56. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -0
  57. package/templates/skills/opera-policy-guard/SKILL.md +26 -0
  58. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -0
  59. package/templates/skills/project-starter-skill/SKILL.md +89 -164
  60. package/templates/skills/project-starter-skill/locales/en/SKILL.md +104 -0
  61. package/ui/css/panels.css +956 -953
  62. package/ui/index.html +1 -1
  63. package/ui/js/api.js +211 -194
  64. package/ui/js/app.js +200 -199
  65. package/ui/js/i18n.js +14 -0
  66. package/ui/js/onboarding.js +439 -437
  67. package/ui/js/state.js +130 -129
  68. package/ui/js/utils.js +175 -172
  69. package/ui/js/views/board.js +255 -254
  70. package/ui/js/views/execution.js +256 -256
  71. package/ui/js/views/insights.js +340 -339
  72. package/ui/js/views/overview.js +366 -361
  73. package/ui/js/views/settings.js +340 -202
  74. package/ui/js/views/sidebar.js +131 -132
  75. package/ui/js/views/skills.js +163 -162
  76. package/ui/js/views/tasks.js +406 -405
  77. package/ui/js/views/topbar.js +239 -183
  78. package/templates/etapa/agent.md +0 -26
  79. package/templates/etapa/genesis.md +0 -94
  80. package/templates/etapa/references/autonomy-and-recovery.md +0 -117
  81. package/templates/etapa/references/etapa-cycle.md +0 -193
  82. package/templates/etapa/registry.md +0 -28
  83. package/templates/etapa/router.md +0 -39
package/bin/trackops.js CHANGED
@@ -1,75 +1,127 @@
1
1
  #!/usr/bin/env node
2
-
2
+
3
3
  const path = require("path");
4
4
  const config = require("../lib/config");
5
+ const runtimeState = require("../lib/runtime-state");
6
+ const { setLocale, t } = require("../lib/i18n");
7
+ const pkg = require("../package.json");
5
8
 
6
9
  const command = process.argv[2];
7
10
  const args = process.argv.slice(3);
8
11
 
12
+ function initCliLocale() {
13
+ let projectLocale = null;
14
+ try {
15
+ const context = config.resolveWorkspaceContext();
16
+ if (context) {
17
+ projectLocale = config.getLocale(config.loadControl(context));
18
+ }
19
+ } catch (_error) {
20
+ projectLocale = null;
21
+ }
22
+ const doctor = runtimeState.doctorLocale(projectLocale);
23
+ setLocale(doctor.effectiveLocale);
24
+ }
25
+
9
26
  function resolveRoot() {
10
- const root = config.resolveProjectRoot();
11
- if (!root) {
12
- console.error("No project_control.json found in this directory or any parent.");
27
+ const context = config.resolveWorkspaceContext();
28
+ if (!context) {
29
+ console.error(t("cli.error.noWorkspace"));
13
30
  process.exit(1);
14
31
  }
15
- return root;
32
+ return context.workspaceRoot;
16
33
  }
17
34
 
18
- function run() {
35
+ async function run() {
36
+ initCliLocale();
19
37
  try {
20
- switch (command) {
21
- case "init":
22
- require("../lib/init").cmdInit(args);
23
- break;
24
-
25
- case "status":
26
- require("../lib/control").cmdStatus(resolveRoot());
27
- break;
28
-
29
- case "next":
30
- require("../lib/control").cmdNext(resolveRoot());
31
- break;
32
-
33
- case "sync":
34
- require("../lib/control").cmdSync(resolveRoot());
35
- break;
36
-
37
- case "dashboard":
38
- require("../lib/server").run();
38
+ switch (command) {
39
+ case "init":
40
+ await require("../lib/init").cmdInit(args);
41
+ break;
42
+
43
+ case "status":
44
+ require("../lib/control").cmdStatus(resolveRoot());
45
+ break;
46
+
47
+ case "next":
48
+ require("../lib/control").cmdNext(resolveRoot());
49
+ break;
50
+
51
+ case "sync":
52
+ require("../lib/control").cmdSync(resolveRoot());
53
+ break;
54
+
55
+ case "dashboard":
56
+ await require("../lib/server").run(args);
57
+ break;
58
+
59
+ case "refresh-repo":
60
+ require("../lib/control").cmdRefreshRepo(resolveRoot(), args);
61
+ break;
62
+
63
+ case "install-hooks":
64
+ require("../lib/control").cmdInstallHooks(resolveRoot());
65
+ break;
66
+
67
+ case "task":
68
+ require("../lib/control").cmdTask(resolveRoot(), args);
69
+ break;
70
+
71
+ case "register":
72
+ require("../lib/registry").cmdRegister(config.resolveProjectRoot() || process.cwd());
39
73
  break;
40
-
41
- case "refresh-repo":
42
- require("../lib/control").cmdRefreshRepo(resolveRoot(), args);
74
+
75
+ case "projects":
76
+ require("../lib/registry").cmdList();
77
+ break;
78
+
79
+ case "workspace": {
80
+ const workspace = require("../lib/workspace");
81
+ const sub = args[0];
82
+ const root = config.resolveProjectRoot() || process.cwd();
83
+ if (sub === "status") workspace.cmdStatus(root);
84
+ else if (sub === "migrate") workspace.cmdMigrate(root, args.slice(1));
85
+ else console.log(t("cli.usage.workspace"));
43
86
  break;
87
+ }
44
88
 
45
- case "install-hooks":
46
- require("../lib/control").cmdInstallHooks(resolveRoot());
89
+ case "env": {
90
+ const env = require("../lib/env");
91
+ const sub = args[0];
92
+ const root = config.resolveProjectRoot() || process.cwd();
93
+ if (sub === "status") env.cmdStatus(root);
94
+ else if (sub === "sync") env.cmdSync(root);
95
+ else console.log(t("cli.usage.env"));
47
96
  break;
97
+ }
48
98
 
49
- case "task":
50
- require("../lib/control").cmdTask(resolveRoot(), args);
99
+ case "release":
100
+ require("../lib/release").cmdRelease(config.resolveProjectRoot() || process.cwd(), args);
51
101
  break;
52
102
 
53
- case "register":
54
- require("../lib/registry").cmdRegister(config.resolveProjectRoot() || process.cwd());
103
+ case "locale":
104
+ require("../lib/preferences").cmdLocale(args, config.resolveProjectRoot() || process.cwd());
55
105
  break;
56
106
 
57
- case "projects":
58
- require("../lib/registry").cmdList();
107
+ case "doctor":
108
+ require("../lib/preferences").cmdDoctor(args, config.resolveProjectRoot() || process.cwd());
59
109
  break;
60
110
 
61
111
  case "opera": {
62
112
  const opera = require("../lib/opera");
63
113
  const sub = args[0];
64
114
  const root = config.resolveProjectRoot() || process.cwd();
65
- if (sub === "install") opera.cmdInstall(root, args.slice(1));
115
+ if (sub === "install") await opera.cmdInstall(root, args.slice(1));
116
+ else if (sub === "bootstrap") await opera.cmdBootstrap(root, args.slice(1));
117
+ else if (sub === "handoff") opera.cmdHandoff(root, args.slice(1));
66
118
  else if (sub === "status") opera.cmdStatus(root);
67
119
  else if (sub === "configure") opera.cmdConfigure(root, args.slice(1));
68
- else if (sub === "upgrade") opera.cmdUpgrade(root);
69
- else { console.log("Usage: trackops opera <install|status|configure|upgrade>"); }
120
+ else if (sub === "upgrade") opera.cmdUpgrade(root, args.slice(1));
121
+ else { console.log(t("cli.usage.opera")); }
70
122
  break;
71
123
  }
72
-
124
+
73
125
  case "skill": {
74
126
  const skills = require("../lib/skills");
75
127
  const sub = args[0];
@@ -78,10 +130,16 @@ function run() {
78
130
  else if (sub === "list") skills.cmdList(root);
79
131
  else if (sub === "remove") skills.cmdRemove(root, args[1]);
80
132
  else if (sub === "catalog") skills.cmdCatalog();
81
- else { console.log("Usage: trackops skill <install|list|remove|catalog> [name]"); }
133
+ else { console.log(t("cli.usage.skill")); }
82
134
  break;
83
135
  }
84
136
 
137
+ case "version":
138
+ case "--version":
139
+ case "-v":
140
+ console.log(pkg.version);
141
+ break;
142
+
85
143
  case "help":
86
144
  case "--help":
87
145
  case "-h":
@@ -90,14 +148,14 @@ function run() {
90
148
  break;
91
149
 
92
150
  default:
93
- console.error(`Unknown command: ${command}`);
94
- console.error("Run 'trackops help' for usage.");
151
+ console.error(t("cli.error.unknownCommand", { command }));
152
+ console.error(t("cli.error.runHelp"));
95
153
  process.exit(1);
96
154
  }
97
- } catch (error) {
98
- console.error(error.message);
99
- process.exit(1);
100
- }
101
- }
102
-
103
- run();
155
+ } catch (error) {
156
+ console.error(error.message);
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ run();
package/lib/config.js CHANGED
@@ -2,57 +2,263 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
+ const { normalizeLocale } = require("./locale");
5
6
 
6
- const DEFAULT_PHASES = [
7
- { id: "O", label: "Orquestar", index: 1 },
8
- { id: "P", label: "Probar", index: 2 },
9
- { id: "E", label: "Estructurar", index: 3 },
10
- { id: "R", label: "Refinar", index: 4 },
11
- { id: "A", label: "Automatizar", index: 5 },
12
- ];
7
+ const DEFAULT_PHASE_IDS = ["O", "P", "E", "R", "A"];
8
+ const DEFAULT_PHASE_LABELS = {
9
+ es: {
10
+ O: "Orquestar",
11
+ P: "Probar",
12
+ E: "Estructurar",
13
+ R: "Refinar",
14
+ A: "Automatizar",
15
+ },
16
+ en: {
17
+ O: "Orchestrate",
18
+ P: "Prove",
19
+ E: "Establish",
20
+ R: "Refine",
21
+ A: "Automate",
22
+ },
23
+ };
13
24
 
14
25
  const DEFAULT_LOCALE = "es";
26
+ const WORKSPACE_MANIFEST = ".trackops-workspace.json";
27
+ const DEFAULT_APP_DIR = "app";
28
+ const DEFAULT_OPS_DIR = "ops";
29
+ const DEFAULT_DEV_BRANCH = "develop";
30
+ const DEFAULT_PUBLISH_BRANCH = "master";
15
31
 
16
- function resolveProjectRoot(startDir) {
32
+ function buildDefaultPhases(locale) {
33
+ const normalized = normalizeLocale(locale) || DEFAULT_LOCALE;
34
+ const labels = DEFAULT_PHASE_LABELS[normalized] || DEFAULT_PHASE_LABELS[DEFAULT_LOCALE];
35
+ return DEFAULT_PHASE_IDS.map((id, index) => ({
36
+ id,
37
+ label: labels[id],
38
+ index: index + 1,
39
+ }));
40
+ }
41
+
42
+ const DEFAULT_PHASES = buildDefaultPhases(DEFAULT_LOCALE);
43
+
44
+ function fileExists(filePath) {
45
+ try {
46
+ return fs.existsSync(filePath);
47
+ } catch (_error) {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ function readJson(filePath, fallback = null) {
53
+ if (!fileExists(filePath)) return fallback;
54
+ try {
55
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
56
+ } catch (_error) {
57
+ return fallback;
58
+ }
59
+ }
60
+
61
+ function createSplitContext(workspaceRoot, manifest = {}) {
62
+ const appDir = manifest.appDir || DEFAULT_APP_DIR;
63
+ const opsDir = manifest.opsDir || DEFAULT_OPS_DIR;
64
+ const workspace = path.resolve(workspaceRoot);
65
+ const appRoot = path.join(workspace, appDir);
66
+ const opsRoot = path.join(workspace, opsDir);
67
+ const env = manifest.env || {};
68
+
69
+ return {
70
+ layout: "split",
71
+ workspaceRoot: workspace,
72
+ root: workspace,
73
+ projectRoot: workspace,
74
+ appRoot,
75
+ opsRoot,
76
+ manifestFile: path.join(workspace, WORKSPACE_MANIFEST),
77
+ packageFile: path.join(appRoot, "package.json"),
78
+ controlFile: path.join(opsRoot, "project_control.json"),
79
+ runtimeFile: path.join(opsRoot, ".tmp", "project-control-runtime.json"),
80
+ docs: {
81
+ taskPlan: path.join(opsRoot, "task_plan.md"),
82
+ progress: path.join(opsRoot, "progress.md"),
83
+ findings: path.join(opsRoot, "findings.md"),
84
+ },
85
+ paths: {
86
+ taskPlan: path.join(opsRoot, "task_plan.md"),
87
+ progress: path.join(opsRoot, "progress.md"),
88
+ findings: path.join(opsRoot, "findings.md"),
89
+ architectureDir: path.join(opsRoot, "architecture"),
90
+ hooksDir: path.join(opsRoot, ".githooks"),
91
+ tmpDir: path.join(opsRoot, ".tmp"),
92
+ bootstrapDir: path.join(opsRoot, "bootstrap"),
93
+ contractDir: path.join(opsRoot, "contract"),
94
+ contractFile: path.join(opsRoot, "contract", "operating-contract.json"),
95
+ policyDir: path.join(opsRoot, "policy"),
96
+ autonomyPolicyFile: path.join(opsRoot, "policy", "autonomy.json"),
97
+ reviewsDir: path.join(opsRoot, "reviews"),
98
+ skillsDir: path.join(opsRoot, ".agents", "skills"),
99
+ registryPath: path.join(opsRoot, ".agents", "skills", "_registry.md"),
100
+ agentHubDir: path.join(opsRoot, ".agent", "hub"),
101
+ genesisFile: path.join(opsRoot, "genesis.md"),
102
+ },
103
+ env: {
104
+ rootFile: path.join(workspace, env.rootFile || ".env"),
105
+ exampleFile: path.join(workspace, env.exampleFile || ".env.example"),
106
+ appBridgeFile: path.join(workspace, env.appBridgeFile || path.join(appDir, ".env")),
107
+ appExampleBridgeFile: path.join(appRoot, ".env.example"),
108
+ bridgeMode: env.bridgeMode || "symlink-or-copy",
109
+ },
110
+ branches: {
111
+ development: manifest.branches?.development || DEFAULT_DEV_BRANCH,
112
+ publish: manifest.branches?.publish || DEFAULT_PUBLISH_BRANCH,
113
+ },
114
+ publish: {
115
+ mode: manifest.publish?.mode || "subtree-flatten",
116
+ sourceDir: manifest.publish?.sourceDir || appDir,
117
+ includeRootFiles: Array.isArray(manifest.publish?.includeRootFiles)
118
+ ? manifest.publish.includeRootFiles
119
+ : [".env.example"],
120
+ requireCleanWorktree: manifest.publish?.requireCleanWorktree !== false,
121
+ },
122
+ };
123
+ }
124
+
125
+ function createLegacyContext(rootDir) {
126
+ const root = path.resolve(rootDir);
127
+ return {
128
+ layout: "legacy",
129
+ workspaceRoot: root,
130
+ root,
131
+ projectRoot: root,
132
+ appRoot: root,
133
+ opsRoot: root,
134
+ manifestFile: null,
135
+ packageFile: path.join(root, "package.json"),
136
+ controlFile: path.join(root, "project_control.json"),
137
+ runtimeFile: path.join(root, ".tmp", "project-control-runtime.json"),
138
+ docs: {
139
+ taskPlan: path.join(root, "task_plan.md"),
140
+ progress: path.join(root, "progress.md"),
141
+ findings: path.join(root, "findings.md"),
142
+ },
143
+ paths: {
144
+ taskPlan: path.join(root, "task_plan.md"),
145
+ progress: path.join(root, "progress.md"),
146
+ findings: path.join(root, "findings.md"),
147
+ architectureDir: path.join(root, "architecture"),
148
+ hooksDir: path.join(root, ".githooks"),
149
+ tmpDir: path.join(root, ".tmp"),
150
+ bootstrapDir: path.join(root, "bootstrap"),
151
+ contractDir: path.join(root, "contract"),
152
+ contractFile: path.join(root, "contract", "operating-contract.json"),
153
+ policyDir: path.join(root, "policy"),
154
+ autonomyPolicyFile: path.join(root, "policy", "autonomy.json"),
155
+ reviewsDir: path.join(root, "reviews"),
156
+ skillsDir: path.join(root, ".agents", "skills"),
157
+ registryPath: path.join(root, ".agents", "skills", "_registry.md"),
158
+ agentHubDir: path.join(root, ".agent", "hub"),
159
+ genesisFile: path.join(root, "genesis.md"),
160
+ },
161
+ env: {
162
+ rootFile: path.join(root, ".env"),
163
+ exampleFile: path.join(root, ".env.example"),
164
+ appBridgeFile: path.join(root, ".env"),
165
+ appExampleBridgeFile: path.join(root, ".env.example"),
166
+ bridgeMode: "none",
167
+ },
168
+ branches: {
169
+ development: DEFAULT_DEV_BRANCH,
170
+ publish: DEFAULT_PUBLISH_BRANCH,
171
+ },
172
+ publish: {
173
+ mode: "legacy",
174
+ sourceDir: ".",
175
+ includeRootFiles: [".env.example"],
176
+ requireCleanWorktree: true,
177
+ },
178
+ };
179
+ }
180
+
181
+ function resolveWorkspaceContext(startDir) {
17
182
  let dir = path.resolve(startDir || process.cwd());
18
183
  const root = path.parse(dir).root;
19
- while (dir !== root) {
20
- if (fs.existsSync(path.join(dir, "project_control.json"))) {
21
- return dir;
184
+ let legacyCandidate = null;
185
+
186
+ while (true) {
187
+ const manifestFile = path.join(dir, WORKSPACE_MANIFEST);
188
+ if (fileExists(manifestFile)) {
189
+ return createSplitContext(dir, readJson(manifestFile, {}) || {});
22
190
  }
191
+
192
+ if (!legacyCandidate && fileExists(path.join(dir, "project_control.json"))) {
193
+ legacyCandidate = dir;
194
+ }
195
+
196
+ if (dir === root) break;
23
197
  dir = path.dirname(dir);
24
198
  }
199
+
200
+ if (legacyCandidate) {
201
+ return createLegacyContext(legacyCandidate);
202
+ }
203
+
25
204
  return null;
26
205
  }
27
206
 
28
- function controlFilePath(root) {
29
- return path.join(root, "project_control.json");
207
+ function ensureContext(contextOrRoot) {
208
+ if (!contextOrRoot) return resolveWorkspaceContext(process.cwd());
209
+ if (typeof contextOrRoot === "object" && contextOrRoot.workspaceRoot) return contextOrRoot;
210
+ const resolved = resolveWorkspaceContext(contextOrRoot);
211
+ if (resolved) return resolved;
212
+ return createLegacyContext(contextOrRoot);
30
213
  }
31
214
 
32
- function runtimeFilePath(root) {
33
- return path.join(root, ".tmp", "project-control-runtime.json");
215
+ function resolveProjectRoot(startDir) {
216
+ const context = resolveWorkspaceContext(startDir);
217
+ return context ? context.workspaceRoot : null;
34
218
  }
35
219
 
36
- function docFilePaths(root) {
37
- return {
38
- taskPlan: path.join(root, "task_plan.md"),
39
- progress: path.join(root, "progress.md"),
40
- findings: path.join(root, "findings.md"),
41
- };
220
+ function controlFilePath(contextOrRoot) {
221
+ return ensureContext(contextOrRoot).controlFile;
222
+ }
223
+
224
+ function runtimeFilePath(contextOrRoot) {
225
+ return ensureContext(contextOrRoot).runtimeFile;
226
+ }
227
+
228
+ function docFilePaths(contextOrRoot) {
229
+ return ensureContext(contextOrRoot).docs;
230
+ }
231
+
232
+ function envFilePaths(contextOrRoot) {
233
+ return ensureContext(contextOrRoot).env;
42
234
  }
43
235
 
44
- function getPhases(control) {
45
- if (
46
- Array.isArray(control.meta?.phases) &&
47
- control.meta.phases.length > 0
48
- ) {
236
+ function packageFilePath(contextOrRoot) {
237
+ return ensureContext(contextOrRoot).packageFile;
238
+ }
239
+
240
+ function workspaceManifestPath(contextOrRoot) {
241
+ return ensureContext(contextOrRoot).manifestFile;
242
+ }
243
+
244
+ function isDefaultPhaseShape(phases) {
245
+ if (!Array.isArray(phases) || phases.length !== DEFAULT_PHASE_IDS.length) return false;
246
+ return phases.every((phase, index) => phase?.id === DEFAULT_PHASE_IDS[index]);
247
+ }
248
+
249
+ function getPhases(control, localeOverride) {
250
+ const locale = normalizeLocale(localeOverride || control.meta?.locale) || DEFAULT_LOCALE;
251
+ if (Array.isArray(control.meta?.phases) && control.meta.phases.length > 0) {
252
+ if (isDefaultPhaseShape(control.meta.phases)) {
253
+ return buildDefaultPhases(locale);
254
+ }
49
255
  return control.meta.phases;
50
256
  }
51
- return DEFAULT_PHASES;
257
+ return buildDefaultPhases(locale);
52
258
  }
53
259
 
54
260
  function getLocale(control) {
55
- return control.meta?.locale || DEFAULT_LOCALE;
261
+ return normalizeLocale(control.meta?.locale) || DEFAULT_LOCALE;
56
262
  }
57
263
 
58
264
  function isOperaInstalled(control) {
@@ -63,35 +269,58 @@ function getOperaVersion(control) {
63
269
  return control.meta?.opera?.version || null;
64
270
  }
65
271
 
66
- // Backwards-compat aliases
67
- function isEtapaInstalled(control) { return isOperaInstalled(control); }
68
- function getEtapaVersion(control) { return getOperaVersion(control); }
69
-
70
- function loadControl(root) {
71
- const filePath = controlFilePath(root);
272
+ function loadControl(contextOrRoot) {
273
+ const filePath = controlFilePath(contextOrRoot);
72
274
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
73
275
  }
74
276
 
75
- function saveControl(root, control) {
277
+ function saveControl(contextOrRoot, control) {
76
278
  control.meta = control.meta || {};
77
279
  control.meta.updatedAt = new Date().toISOString();
78
- const filePath = controlFilePath(root);
280
+ const filePath = controlFilePath(contextOrRoot);
281
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
79
282
  fs.writeFileSync(filePath, JSON.stringify(control, null, 2) + "\n", "utf8");
80
283
  }
81
284
 
285
+ function loadWorkspaceManifest(contextOrRoot) {
286
+ const context = ensureContext(contextOrRoot);
287
+ if (!context.manifestFile || !fileExists(context.manifestFile)) return null;
288
+ return readJson(context.manifestFile, null);
289
+ }
290
+
291
+ function saveWorkspaceManifest(contextOrRoot, manifest) {
292
+ const context = ensureContext(contextOrRoot);
293
+ const manifestFile = context.manifestFile || path.join(context.workspaceRoot, WORKSPACE_MANIFEST);
294
+ fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
295
+ }
296
+
82
297
  module.exports = {
83
298
  DEFAULT_PHASES,
299
+ DEFAULT_PHASE_IDS,
84
300
  DEFAULT_LOCALE,
301
+ DEFAULT_APP_DIR,
302
+ DEFAULT_OPS_DIR,
303
+ DEFAULT_DEV_BRANCH,
304
+ DEFAULT_PUBLISH_BRANCH,
305
+ WORKSPACE_MANIFEST,
306
+ buildDefaultPhases,
307
+ createSplitContext,
308
+ createLegacyContext,
309
+ resolveWorkspaceContext,
85
310
  resolveProjectRoot,
311
+ ensureContext,
86
312
  controlFilePath,
87
313
  runtimeFilePath,
88
314
  docFilePaths,
315
+ envFilePaths,
316
+ packageFilePath,
317
+ workspaceManifestPath,
318
+ loadWorkspaceManifest,
319
+ saveWorkspaceManifest,
89
320
  getPhases,
90
321
  getLocale,
91
322
  isOperaInstalled,
92
323
  getOperaVersion,
93
- isEtapaInstalled,
94
- getEtapaVersion,
95
324
  loadControl,
96
325
  saveControl,
97
326
  };