trackops 2.0.4 → 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.
Files changed (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +660 -575
  3. package/bin/trackops.js +127 -106
  4. package/lib/cli-format.js +118 -0
  5. package/lib/config.js +352 -326
  6. package/lib/control.js +408 -246
  7. package/lib/env.js +234 -222
  8. package/lib/i18n.js +5 -4
  9. package/lib/init.js +390 -282
  10. package/lib/locale.js +41 -41
  11. package/lib/opera-bootstrap.js +1066 -880
  12. package/lib/opera.js +615 -444
  13. package/lib/preferences.js +74 -74
  14. package/lib/registry.js +214 -214
  15. package/lib/release.js +56 -56
  16. package/lib/runtime-state.js +144 -144
  17. package/lib/skills.js +114 -89
  18. package/lib/workspace.js +259 -248
  19. package/locales/en.json +311 -167
  20. package/locales/es.json +314 -170
  21. package/package.json +61 -58
  22. package/scripts/postinstall-locale.js +21 -21
  23. package/scripts/skills-marketplace-smoke.js +124 -124
  24. package/scripts/smoke-tests.js +563 -517
  25. package/scripts/sync-skill-version.js +21 -21
  26. package/scripts/validate-skill.js +103 -103
  27. package/skills/trackops/SKILL.md +126 -122
  28. package/skills/trackops/agents/openai.yaml +7 -7
  29. package/skills/trackops/locales/en/SKILL.md +126 -122
  30. package/skills/trackops/locales/en/references/activation.md +94 -90
  31. package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
  32. package/skills/trackops/locales/en/references/workflow.md +55 -32
  33. package/skills/trackops/references/activation.md +94 -90
  34. package/skills/trackops/references/troubleshooting.md +73 -67
  35. package/skills/trackops/references/workflow.md +55 -32
  36. package/skills/trackops/skill.json +29 -29
  37. package/templates/hooks/post-checkout +2 -2
  38. package/templates/hooks/post-commit +2 -2
  39. package/templates/hooks/post-merge +2 -2
  40. package/templates/opera/agent.md +28 -27
  41. package/templates/opera/architecture/dependency-graph.md +24 -24
  42. package/templates/opera/architecture/runtime-automation.md +24 -24
  43. package/templates/opera/architecture/runtime-operations.md +34 -34
  44. package/templates/opera/en/agent.md +22 -21
  45. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  46. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  47. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  48. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  49. package/templates/opera/en/reviews/integration-audit.md +18 -18
  50. package/templates/opera/en/router.md +24 -19
  51. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  52. package/templates/opera/references/opera-cycle.md +193 -193
  53. package/templates/opera/registry.md +28 -28
  54. package/templates/opera/reviews/delivery-audit.md +18 -18
  55. package/templates/opera/reviews/integration-audit.md +18 -18
  56. package/templates/opera/router.md +54 -49
  57. package/templates/skills/changelog-updater/SKILL.md +69 -69
  58. package/templates/skills/commiter/SKILL.md +99 -99
  59. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  60. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  61. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  62. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  63. package/templates/skills/opera-skill/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  65. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  66. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  67. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  68. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  69. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  70. package/ui/css/base.css +284 -284
  71. package/ui/css/charts.css +425 -425
  72. package/ui/css/components.css +1107 -1107
  73. package/ui/css/onboarding.css +133 -133
  74. package/ui/css/terminal.css +125 -125
  75. package/ui/css/timeline.css +58 -58
  76. package/ui/css/tokens.css +284 -284
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -99
  79. package/ui/js/charts.js +526 -526
  80. package/ui/js/console-logger.js +172 -172
  81. package/ui/js/filters.js +247 -247
  82. package/ui/js/icons.js +129 -129
  83. package/ui/js/keyboard.js +229 -229
  84. package/ui/js/router.js +142 -142
  85. package/ui/js/theme.js +100 -100
  86. package/ui/js/time-tracker.js +248 -248
  87. package/ui/js/views/dashboard.js +870 -870
  88. package/ui/js/views/flash.js +47 -47
  89. package/ui/js/views/projects.js +745 -745
  90. package/ui/js/views/scrum.js +476 -476
  91. package/ui/js/views/settings.js +331 -331
  92. package/ui/js/views/timeline.js +265 -265
package/lib/init.js CHANGED
@@ -1,325 +1,433 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { spawnSync } = require("child_process");
6
-
7
- const config = require("./config");
8
- const registry = require("./registry");
9
- const env = require("./env");
10
- const workspace = require("./workspace");
11
- const { t, setLocale } = require("./i18n");
12
- const { detectSystemLocale, promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
13
- const runtimeState = require("./runtime-state");
14
-
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const config = require("./config");
8
+ const registry = require("./registry");
9
+ const env = require("./env");
10
+ const workspace = require("./workspace");
11
+ const { t, setLocale } = require("./i18n");
12
+ const { detectSystemLocale, promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
13
+ const runtimeState = require("./runtime-state");
14
+
15
15
  const GENERATED_SCRIPT_COMMANDS = {
16
- ops: "npx --yes trackops",
17
- "ops:help": "npx --yes trackops help",
18
- "ops:dashboard": "npx --yes trackops dashboard",
19
- "ops:status": "npx --yes trackops status",
20
- "ops:next": "npx --yes trackops next",
21
- "ops:sync": "npx --yes trackops sync",
16
+ ops: "npx --yes trackops",
17
+ "ops:help": "npx --yes trackops help",
18
+ "ops:dashboard": "npx --yes trackops dashboard",
19
+ "ops:status": "npx --yes trackops status",
20
+ "ops:next": "npx --yes trackops next",
21
+ "ops:sync": "npx --yes trackops sync",
22
22
  "ops:repo": "npx --yes trackops refresh-repo",
23
23
  };
24
24
 
25
- function nowIso() {
26
- return new Date().toISOString();
25
+ const ROOT_PRESERVED_ENTRIES = new Set([
26
+ ".git",
27
+ ".gitignore",
28
+ ".gitattributes",
29
+ ".gitmodules",
30
+ ".editorconfig",
31
+ ".env",
32
+ ".env.example",
33
+ ".vscode",
34
+ ".idea",
35
+ ".nvmrc",
36
+ ".node-version",
37
+ ".tool-versions",
38
+ ".npmrc",
39
+ ".yarnrc.yml",
40
+ ".pnp.cjs",
41
+ ".pnp.loader.mjs",
42
+ "pnpm-workspace.yaml",
43
+ "turbo.json",
44
+ ]);
45
+
46
+ function nowIso() {
47
+ return new Date().toISOString();
48
+ }
49
+
50
+ function parseArgs(args) {
51
+ const options = {
52
+ locale: null,
53
+ name: null,
54
+ withOpera: false,
55
+ phases: null,
56
+ noBootstrap: false,
57
+ legacyLayout: false,
58
+ bootstrapMode: "auto",
59
+ technicalLevel: null,
60
+ projectState: null,
61
+ docsState: null,
62
+ decisionOwnership: null,
63
+ };
64
+ for (let i = 0; i < args.length; i += 1) {
65
+ if (args[i] === "--locale" && args[i + 1]) { options.locale = args[i + 1]; i += 1; }
66
+ else if (args[i] === "--name" && args[i + 1]) { options.name = args[i + 1]; i += 1; }
67
+ else if (args[i] === "--with-opera") { options.withOpera = true; }
68
+ else if (args[i] === "--no-bootstrap") { options.noBootstrap = true; }
69
+ else if (args[i] === "--legacy-layout") { options.legacyLayout = true; }
70
+ else if (args[i] === "--bootstrap-mode" && args[i + 1]) { options.bootstrapMode = args[i + 1]; i += 1; }
71
+ else if (args[i] === "--technical-level" && args[i + 1]) { options.technicalLevel = args[i + 1]; i += 1; }
72
+ else if (args[i] === "--project-state" && args[i + 1]) { options.projectState = args[i + 1]; i += 1; }
73
+ else if (args[i] === "--docs-state" && args[i + 1]) { options.docsState = args[i + 1]; i += 1; }
74
+ else if (args[i] === "--decision-ownership" && args[i + 1]) { options.decisionOwnership = args[i + 1]; i += 1; }
75
+ else if (args[i] === "--phases" && args[i + 1]) {
76
+ try { options.phases = JSON.parse(args[i + 1]); } catch (_e) { /* ignore */ }
77
+ i += 1;
78
+ }
79
+ }
80
+ return options;
81
+ }
82
+
83
+ function detectProjectName(root) {
84
+ const pkgPath = path.join(root, "package.json");
85
+ if (fs.existsSync(pkgPath)) {
86
+ try {
87
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
88
+ return pkg.displayName || pkg.name || path.basename(root);
89
+ } catch (_e) {
90
+ // noop
91
+ }
92
+ }
93
+ return path.basename(root);
94
+ }
95
+
96
+ function buildDefaultControl(context, options) {
97
+ const locale = resolveLocale(options.locale, runtimeState.getGlobalLocale() || config.DEFAULT_LOCALE);
98
+ const phases = options.phases || config.buildDefaultPhases(locale);
99
+ const isSplit = context.layout === "split";
100
+ setLocale(locale);
101
+
102
+ return {
103
+ meta: {
104
+ projectName: options.name || "My Project",
105
+ controlVersion: 2,
106
+ locale,
107
+ phases,
108
+ updatedAt: nowIso(),
109
+ executionSource: "project_control.json",
110
+ currentFocus: t("init.defaultFocus"),
111
+ focusPhase: phases[0]?.id || "O",
112
+ deliveryTarget: t("init.defaultTarget"),
113
+ workspace: isSplit ? {
114
+ layout: "split",
115
+ workspaceRoot: ".",
116
+ appDir: path.basename(context.appRoot),
117
+ opsDir: path.basename(context.opsRoot),
118
+ developmentBranch: context.branches.development,
119
+ publishBranch: context.branches.publish,
120
+ publishMode: context.publish.mode,
121
+ } : undefined,
122
+ environment: {
123
+ rootEnvFile: path.relative(context.workspaceRoot, context.env.rootFile).replace(/\\/g, "/"),
124
+ exampleFile: path.relative(context.workspaceRoot, context.env.exampleFile).replace(/\\/g, "/"),
125
+ appBridgeFile: path.relative(context.workspaceRoot, context.env.appBridgeFile).replace(/\\/g, "/"),
126
+ bridgeMode: isSplit ? "symlink-or-copy" : "none",
127
+ requiredKeys: [],
128
+ optionalKeys: [],
129
+ lastAuditAt: null,
130
+ },
131
+ userProfile: {
132
+ technicalLevel: null,
133
+ explanationMode: null,
134
+ capturedAt: null,
135
+ },
136
+ discovery: {
137
+ projectState: null,
138
+ documentationState: null,
139
+ availableArtifacts: [],
140
+ },
141
+ },
142
+ checks: {
143
+ lastBuild: { status: "pending", date: null, note: "" },
144
+ lastTest: { status: "pending", date: null, note: "" },
145
+ lastReview: { status: "pending", date: null, note: "" },
146
+ },
147
+ rituals: {
148
+ startOfSession: ["trackops status", "trackops next"],
149
+ beforeImplementation: ["trackops task start <task-id>"],
150
+ endOfSession: ["trackops task review|complete|block <task-id> <note>", "trackops sync"],
151
+ beforeCommit: ["trackops status", "trackops sync"],
152
+ },
153
+ milestones: [],
154
+ decisionsPending: [],
155
+ tasks: [
156
+ {
157
+ id: "ops-bootstrap",
158
+ title: t("init.defaultTaskTitle"),
159
+ phase: phases[0]?.id || "O",
160
+ stream: "Operations",
161
+ priority: "P0",
162
+ status: "pending",
163
+ required: true,
164
+ dependsOn: [],
165
+ summary: t("init.defaultTaskSummary"),
166
+ acceptance: [],
167
+ history: [{ at: nowIso(), action: "create", note: "trackops init" }],
168
+ },
169
+ ],
170
+ findings: [],
171
+ };
172
+ }
173
+
174
+ function upsertScripts(root) {
175
+ const pkgPath = path.join(root, "package.json");
176
+ let pkg = {};
177
+ if (fs.existsSync(pkgPath)) {
178
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch (_e) { /* ignore */ }
179
+ }
180
+ pkg.scripts = pkg.scripts || {};
181
+ Object.assign(pkg.scripts, GENERATED_SCRIPT_COMMANDS);
182
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
183
+ }
184
+
185
+ function installHooks(context) {
186
+ const hooksDir = context.paths.hooksDir;
187
+ fs.mkdirSync(hooksDir, { recursive: true });
188
+
189
+ const relativeCommand = context.layout === "split"
190
+ ? "npx --yes trackops refresh-repo --quiet >/dev/null 2>&1 || true\n"
191
+ : "npx --yes trackops refresh-repo --quiet >/dev/null 2>&1 || true\n";
192
+ const hookContent = `#!/bin/sh\n${relativeCommand}`;
193
+ for (const hookName of ["post-commit", "post-checkout", "post-merge"]) {
194
+ const hookPath = path.join(hooksDir, hookName);
195
+ fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
196
+ }
197
+
198
+ if (fs.existsSync(path.join(context.workspaceRoot, ".git"))) {
199
+ const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
200
+ const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
201
+ if (result.status !== 0) {
202
+ console.log(t("cli.hooksError"));
203
+ }
204
+ }
205
+ }
206
+
207
+ function ensureTmpDir(context) {
208
+ fs.mkdirSync(context.paths.tmpDir, { recursive: true });
209
+ const gitkeep = path.join(context.paths.tmpDir, ".gitkeep");
210
+ if (!fs.existsSync(gitkeep)) {
211
+ fs.writeFileSync(gitkeep, "", "utf8");
212
+ }
27
213
  }
28
214
 
29
- function parseArgs(args) {
30
- const options = {
31
- locale: null,
32
- name: null,
33
- withOpera: false,
34
- phases: null,
35
- noBootstrap: false,
36
- legacyLayout: false,
37
- bootstrapMode: "auto",
38
- technicalLevel: null,
39
- projectState: null,
40
- docsState: null,
41
- decisionOwnership: null,
42
- };
43
- for (let i = 0; i < args.length; i += 1) {
44
- if (args[i] === "--locale" && args[i + 1]) { options.locale = args[i + 1]; i += 1; }
45
- else if (args[i] === "--name" && args[i + 1]) { options.name = args[i + 1]; i += 1; }
46
- else if (args[i] === "--with-opera") { options.withOpera = true; }
47
- else if (args[i] === "--no-bootstrap") { options.noBootstrap = true; }
48
- else if (args[i] === "--legacy-layout") { options.legacyLayout = true; }
49
- else if (args[i] === "--bootstrap-mode" && args[i + 1]) { options.bootstrapMode = args[i + 1]; i += 1; }
50
- else if (args[i] === "--technical-level" && args[i + 1]) { options.technicalLevel = args[i + 1]; i += 1; }
51
- else if (args[i] === "--project-state" && args[i + 1]) { options.projectState = args[i + 1]; i += 1; }
52
- else if (args[i] === "--docs-state" && args[i + 1]) { options.docsState = args[i + 1]; i += 1; }
53
- else if (args[i] === "--decision-ownership" && args[i + 1]) { options.decisionOwnership = args[i + 1]; i += 1; }
54
- else if (args[i] === "--phases" && args[i + 1]) {
55
- try { options.phases = JSON.parse(args[i + 1]); } catch (_e) { /* ignore */ }
56
- i += 1;
57
- }
58
- }
59
- return options;
215
+ function isRetryableMoveError(error) {
216
+ return ["EPERM", "EXDEV", "EBUSY", "ENOTEMPTY"].includes(error?.code);
60
217
  }
61
218
 
62
- function detectProjectName(root) {
63
- const pkgPath = path.join(root, "package.json");
64
- if (fs.existsSync(pkgPath)) {
65
- try {
66
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
67
- return pkg.displayName || pkg.name || path.basename(root);
68
- } catch (_e) {
69
- // noop
219
+ function moveEntry(fromPath, toPath) {
220
+ if (!fs.existsSync(fromPath)) return;
221
+ fs.mkdirSync(path.dirname(toPath), { recursive: true });
222
+ try {
223
+ fs.renameSync(fromPath, toPath);
224
+ } catch (error) {
225
+ if (!isRetryableMoveError(error)) throw error;
226
+ const stat = fs.statSync(fromPath);
227
+ if (stat.isDirectory()) {
228
+ fs.cpSync(fromPath, toPath, { recursive: true, force: true });
229
+ fs.rmSync(fromPath, { recursive: true, force: true });
230
+ return;
70
231
  }
232
+ fs.copyFileSync(fromPath, toPath);
233
+ fs.rmSync(fromPath, { force: true });
71
234
  }
72
- return path.basename(root);
73
235
  }
74
236
 
75
- function buildDefaultControl(context, options) {
76
- const locale = resolveLocale(options.locale, runtimeState.getGlobalLocale() || config.DEFAULT_LOCALE);
77
- const phases = options.phases || config.buildDefaultPhases(locale);
78
- const isSplit = context.layout === "split";
79
- setLocale(locale);
80
-
81
- return {
82
- meta: {
83
- projectName: options.name || "My Project",
84
- controlVersion: 2,
85
- locale,
86
- phases,
87
- updatedAt: nowIso(),
88
- executionSource: "project_control.json",
89
- currentFocus: t("init.defaultFocus"),
90
- focusPhase: phases[0]?.id || "O",
91
- deliveryTarget: t("init.defaultTarget"),
92
- workspace: isSplit ? {
93
- layout: "split",
94
- workspaceRoot: ".",
95
- appDir: path.basename(context.appRoot),
96
- opsDir: path.basename(context.opsRoot),
97
- developmentBranch: context.branches.development,
98
- publishBranch: context.branches.publish,
99
- publishMode: context.publish.mode,
100
- } : undefined,
101
- environment: {
102
- rootEnvFile: path.relative(context.workspaceRoot, context.env.rootFile).replace(/\\/g, "/"),
103
- exampleFile: path.relative(context.workspaceRoot, context.env.exampleFile).replace(/\\/g, "/"),
104
- appBridgeFile: path.relative(context.workspaceRoot, context.env.appBridgeFile).replace(/\\/g, "/"),
105
- bridgeMode: isSplit ? "symlink-or-copy" : "none",
106
- requiredKeys: [],
107
- optionalKeys: [],
108
- lastAuditAt: null,
109
- },
110
- userProfile: {
111
- technicalLevel: null,
112
- explanationMode: null,
113
- capturedAt: null,
114
- },
115
- discovery: {
116
- projectState: null,
117
- documentationState: null,
118
- availableArtifacts: [],
119
- },
120
- },
121
- checks: {
122
- lastBuild: { status: "pending", date: null, note: "" },
123
- lastTest: { status: "pending", date: null, note: "" },
124
- lastReview: { status: "pending", date: null, note: "" },
125
- },
126
- rituals: {
127
- startOfSession: ["trackops status", "trackops next"],
128
- beforeImplementation: ["trackops task start <task-id>"],
129
- endOfSession: ["trackops task review|complete|block <task-id> <note>", "trackops sync"],
130
- beforeCommit: ["trackops status", "trackops sync"],
131
- },
132
- milestones: [],
133
- decisionsPending: [],
134
- tasks: [
135
- {
136
- id: "ops-bootstrap",
137
- title: t("init.defaultTaskTitle"),
138
- phase: phases[0]?.id || "O",
139
- stream: "Operations",
140
- priority: "P0",
141
- status: "pending",
142
- required: true,
143
- dependsOn: [],
144
- summary: t("init.defaultTaskSummary"),
145
- acceptance: [],
146
- history: [{ at: nowIso(), action: "create", note: "trackops init" }],
147
- },
148
- ],
149
- findings: [],
150
- };
151
- }
152
-
153
- function upsertScripts(root) {
154
- const pkgPath = path.join(root, "package.json");
155
- let pkg = {};
156
- if (fs.existsSync(pkgPath)) {
157
- try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch (_e) { /* ignore */ }
158
- }
159
- pkg.scripts = pkg.scripts || {};
160
- Object.assign(pkg.scripts, GENERATED_SCRIPT_COMMANDS);
161
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
237
+ function collectRootEntries(targetRoot) {
238
+ return fs.readdirSync(targetRoot, { withFileTypes: true })
239
+ .filter((entry) => ![".", ".."].includes(entry.name));
162
240
  }
163
241
 
164
- function installHooks(context) {
165
- const hooksDir = context.paths.hooksDir;
166
- fs.mkdirSync(hooksDir, { recursive: true });
242
+ function analyzeSplitInit(targetRoot) {
243
+ const entries = collectRootEntries(targetRoot);
244
+ const manifestPath = path.join(targetRoot, config.WORKSPACE_MANIFEST);
245
+ const legacyControlPath = path.join(targetRoot, "project_control.json");
246
+ const hasManifest = fs.existsSync(manifestPath);
167
247
 
168
- const relativeCommand = context.layout === "split"
169
- ? "npx --yes trackops refresh-repo --quiet >/dev/null 2>&1 || true\n"
170
- : "npx --yes trackops refresh-repo --quiet >/dev/null 2>&1 || true\n";
171
- const hookContent = `#!/bin/sh\n${relativeCommand}`;
172
- for (const hookName of ["post-commit", "post-checkout", "post-merge"]) {
173
- const hookPath = path.join(hooksDir, hookName);
174
- fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
248
+ if (hasManifest) {
249
+ return { mode: "upgrade", entries };
175
250
  }
176
251
 
177
- if (fs.existsSync(path.join(context.workspaceRoot, ".git"))) {
178
- const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
179
- spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
252
+ if (fs.existsSync(legacyControlPath)) {
253
+ throw new Error(t("init.error.legacyDetected"));
180
254
  }
181
- }
182
255
 
183
- function ensureTmpDir(context) {
184
- fs.mkdirSync(context.paths.tmpDir, { recursive: true });
185
- const gitkeep = path.join(context.paths.tmpDir, ".gitkeep");
186
- if (!fs.existsSync(gitkeep)) {
187
- fs.writeFileSync(gitkeep, "", "utf8");
256
+ const conflicts = entries
257
+ .map((entry) => entry.name)
258
+ .filter((name) => [config.DEFAULT_APP_DIR, config.DEFAULT_OPS_DIR, config.WORKSPACE_MANIFEST].includes(name));
259
+
260
+ if (conflicts.length) {
261
+ throw new Error(t("init.error.reservedConflict", { entries: conflicts.join(", ") }));
188
262
  }
263
+
264
+ const movableEntries = entries.filter((entry) => !ROOT_PRESERVED_ENTRIES.has(entry.name));
265
+ return {
266
+ mode: movableEntries.length ? "adopt" : "new",
267
+ entries,
268
+ movableEntries,
269
+ };
189
270
  }
190
271
 
191
- function ensureSplitLayoutAllowed(targetRoot) {
192
- const entries = fs.readdirSync(targetRoot, { withFileTypes: true });
193
- const meaningful = entries.filter((entry) => ![".git"].includes(entry.name));
194
- if (meaningful.length > 0) {
195
- throw new Error("Directory is not empty. Run 'trackops workspace migrate' to convert an existing project.");
272
+ function adoptExistingProject(targetRoot, appRoot, movableEntries) {
273
+ for (const entry of movableEntries) {
274
+ moveEntry(path.join(targetRoot, entry.name), path.join(appRoot, entry.name));
196
275
  }
197
276
  }
198
277
 
199
278
  function initSplitProject(root, options) {
200
279
  const targetRoot = path.resolve(root);
201
280
  fs.mkdirSync(targetRoot, { recursive: true });
202
- ensureSplitLayoutAllowed(targetRoot);
203
-
281
+ const projectName = options.name || detectProjectName(targetRoot);
282
+ const initMode = analyzeSplitInit(targetRoot);
204
283
  const manifest = workspace.buildManifest();
205
284
  const context = config.createSplitContext(targetRoot, manifest);
285
+
286
+ if (initMode.mode === "adopt") {
287
+ fs.mkdirSync(context.appRoot, { recursive: true });
288
+ adoptExistingProject(targetRoot, context.appRoot, initMode.movableEntries);
289
+ }
290
+
206
291
  fs.mkdirSync(context.appRoot, { recursive: true });
207
292
  fs.mkdirSync(context.opsRoot, { recursive: true });
208
293
  config.saveWorkspaceManifest(context, manifest);
209
294
  workspace.ensureRootGitignore(targetRoot);
210
295
 
211
- const control = buildDefaultControl(context, options);
212
- control.meta.projectName = options.name || detectProjectName(context.workspaceRoot);
213
- config.saveControl(context, control);
214
-
215
- installHooks(context);
216
- ensureTmpDir(context);
217
- const controlApi = require("./control");
218
- controlApi.syncDocs(context, control);
219
- env.syncEnvironment(context, control);
220
-
221
- try {
222
- registry.registerProject(context.workspaceRoot);
223
- } catch (_e) {
224
- // ignore
225
- }
226
-
227
- console.log(t("init.created", { file: ".trackops-workspace.json" }));
228
- console.log(t("init.created", { file: "ops/project_control.json" }));
229
- console.log(t("init.created", { file: "ops/.githooks/" }));
230
- console.log(t("init.created", { file: ".env" }));
231
- console.log(t("init.created", { file: ".env.example" }));
232
- console.log("");
233
- console.log(t("init.welcome"));
234
-
235
- return { root: context.workspaceRoot, context, isUpgrade: false, operaDetected: false };
236
- }
237
-
238
- function initLegacyProject(root, options) {
239
- const targetRoot = path.resolve(root);
240
- const context = config.createLegacyContext(targetRoot);
241
- const controlFile = context.controlFile;
242
- const isUpgrade = fs.existsSync(controlFile);
243
- options.locale = resolveLocale(options.locale, detectSystemLocale());
244
-
245
- if (!options.name) {
246
- options.name = detectProjectName(targetRoot);
247
- }
248
-
296
+ const controlFile = context.controlFile || path.join(context.opsRoot, "project_control.json");
297
+ const isUpgrade = initMode.mode === "upgrade" && fs.existsSync(controlFile);
298
+ let control;
249
299
  if (isUpgrade) {
250
- const existing = JSON.parse(fs.readFileSync(controlFile, "utf8"));
251
- if (!existing.meta.phases) existing.meta.phases = options.phases || config.buildDefaultPhases(options.locale);
252
- if (!existing.meta.locale) existing.meta.locale = options.locale || config.DEFAULT_LOCALE;
253
- if (!existing.meta.controlVersion || existing.meta.controlVersion < 2) existing.meta.controlVersion = 2;
254
- existing.meta.updatedAt = nowIso();
255
- fs.writeFileSync(controlFile, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
256
- console.log(t("init.updated", { file: "project_control.json" }));
300
+ control = JSON.parse(fs.readFileSync(controlFile, "utf8"));
301
+ if (!control.meta.phases) control.meta.phases = options.phases || config.buildDefaultPhases(options.locale);
302
+ if (!control.meta.locale) control.meta.locale = options.locale || config.DEFAULT_LOCALE;
303
+ if (!control.meta.controlVersion || control.meta.controlVersion < 2) control.meta.controlVersion = 2;
304
+ control.meta.updatedAt = nowIso();
305
+ config.saveControl(context, control);
257
306
  } else {
258
- const control = buildDefaultControl(context, options);
259
- control.meta.projectName = options.name;
260
- fs.writeFileSync(controlFile, `${JSON.stringify(control, null, 2)}\n`, "utf8");
261
- console.log(t("init.created", { file: "project_control.json" }));
307
+ control = buildDefaultControl(context, options);
308
+ control.meta.projectName = projectName;
309
+ config.saveControl(context, control);
262
310
  }
263
-
264
- upsertScripts(targetRoot);
265
- console.log(t("init.updated", { file: "package.json" }));
266
-
267
- installHooks(context);
268
- console.log(t("init.created", { file: ".githooks/" }));
269
- ensureTmpDir(context);
270
-
271
- try {
272
- registry.registerProject(targetRoot);
273
- console.log(t("init.registered"));
274
- } catch (_e) {
275
- // ignore
311
+
312
+ installHooks(context);
313
+ ensureTmpDir(context);
314
+ const controlApi = require("./control");
315
+ controlApi.syncDocs(context, control);
316
+ env.syncEnvironment(context, control);
317
+
318
+ try {
319
+ registry.registerProject(context.workspaceRoot);
320
+ } catch (_e) {
321
+ // ignore
322
+ }
323
+
324
+ if (isUpgrade) {
325
+ console.log(t("init.updated", { file: ".trackops-workspace.json" }));
326
+ console.log(t("init.updated", { file: "ops/project_control.json" }));
327
+ console.log(t("init.updated", { file: "ops/.githooks/" }));
328
+ console.log(t("init.updated", { file: ".env" }));
329
+ console.log(t("init.updated", { file: ".env.example" }));
330
+ } else {
331
+ console.log(t("init.created", { file: ".trackops-workspace.json" }));
332
+ console.log(t("init.created", { file: "ops/project_control.json" }));
333
+ console.log(t("init.created", { file: "ops/.githooks/" }));
334
+ console.log(t("init.created", { file: ".env" }));
335
+ console.log(t("init.created", { file: ".env.example" }));
336
+ if (initMode.mode === "adopt") {
337
+ console.log(t("init.adoptedExistingRepo", { dir: path.basename(context.appRoot) }));
338
+ }
276
339
  }
277
-
278
340
  console.log("");
279
341
  console.log(t("init.welcome"));
280
- return { root: targetRoot, context, isUpgrade, operaDetected: false };
281
- }
282
-
283
- function initProject(root, options) {
284
- const normalized = { ...(options || {}) };
285
- normalized.locale = resolveLocale(normalized.locale, runtimeState.getGlobalLocale() || config.DEFAULT_LOCALE);
286
- setLocale(normalized.locale);
287
- if (normalized.legacyLayout) {
288
- return initLegacyProject(root, normalized);
289
- }
290
- return initSplitProject(root, normalized);
291
- }
292
342
 
293
- async function cmdInit(args) {
294
- const options = parseArgs(args || []);
295
- const explicitLocale = resolveLocale(options.locale, null);
296
- const globalLocale = runtimeState.getGlobalLocale();
297
- if (explicitLocale) {
298
- options.locale = explicitLocale;
299
- } else if (!globalLocale) {
300
- options.locale = await promptForLocale(detectSystemLocale());
301
- } else {
302
- options.locale = await maybePromptForLocale(globalLocale, { promptMode: "always" });
303
- }
304
- if (!globalLocale) {
305
- await runtimeState.ensureGlobalLocale({ preferredLocale: options.locale, interactive: false });
306
- }
307
- setLocale(options.locale || config.DEFAULT_LOCALE);
308
-
309
- const result = initProject(process.cwd(), options);
310
-
311
- if (options.withOpera) {
312
- const opera = require("./opera");
313
- await opera.install(result.root, {
314
- locale: options.locale,
315
- bootstrap: !options.noBootstrap,
316
- bootstrapMode: options.bootstrapMode,
317
- technicalLevel: options.technicalLevel,
318
- projectState: options.projectState,
319
- docsState: options.docsState,
320
- decisionOwnership: options.decisionOwnership,
321
- });
322
- }
343
+ return { root: context.workspaceRoot, context, isUpgrade, operaDetected: false };
323
344
  }
324
-
325
- module.exports = { initProject, cmdInit, buildDefaultControl };
345
+
346
+ function initLegacyProject(root, options) {
347
+ const targetRoot = path.resolve(root);
348
+ const context = config.createLegacyContext(targetRoot);
349
+ const controlFile = context.controlFile;
350
+ const isUpgrade = fs.existsSync(controlFile);
351
+ options.locale = resolveLocale(options.locale, detectSystemLocale());
352
+
353
+ if (!options.name) {
354
+ options.name = detectProjectName(targetRoot);
355
+ }
356
+
357
+ if (isUpgrade) {
358
+ const existing = JSON.parse(fs.readFileSync(controlFile, "utf8"));
359
+ if (!existing.meta.phases) existing.meta.phases = options.phases || config.buildDefaultPhases(options.locale);
360
+ if (!existing.meta.locale) existing.meta.locale = options.locale || config.DEFAULT_LOCALE;
361
+ if (!existing.meta.controlVersion || existing.meta.controlVersion < 2) existing.meta.controlVersion = 2;
362
+ existing.meta.updatedAt = nowIso();
363
+ fs.writeFileSync(controlFile, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
364
+ console.log(t("init.updated", { file: "project_control.json" }));
365
+ } else {
366
+ const control = buildDefaultControl(context, options);
367
+ control.meta.projectName = options.name;
368
+ fs.writeFileSync(controlFile, `${JSON.stringify(control, null, 2)}\n`, "utf8");
369
+ console.log(t("init.created", { file: "project_control.json" }));
370
+ }
371
+
372
+ upsertScripts(targetRoot);
373
+ console.log(t("init.updated", { file: "package.json" }));
374
+
375
+ installHooks(context);
376
+ console.log(t("init.created", { file: ".githooks/" }));
377
+ ensureTmpDir(context);
378
+
379
+ try {
380
+ registry.registerProject(targetRoot);
381
+ console.log(t("init.registered"));
382
+ } catch (_e) {
383
+ // ignore
384
+ }
385
+
386
+ console.log("");
387
+ console.log(t("init.welcome"));
388
+ return { root: targetRoot, context, isUpgrade, operaDetected: false };
389
+ }
390
+
391
+ function initProject(root, options) {
392
+ const normalized = { ...(options || {}) };
393
+ normalized.locale = resolveLocale(normalized.locale, runtimeState.getGlobalLocale() || config.DEFAULT_LOCALE);
394
+ setLocale(normalized.locale);
395
+ if (normalized.legacyLayout) {
396
+ return initLegacyProject(root, normalized);
397
+ }
398
+ return initSplitProject(root, normalized);
399
+ }
400
+
401
+ async function cmdInit(args) {
402
+ const options = parseArgs(args || []);
403
+ const explicitLocale = resolveLocale(options.locale, null);
404
+ const globalLocale = runtimeState.getGlobalLocale();
405
+ if (explicitLocale) {
406
+ options.locale = explicitLocale;
407
+ } else if (!globalLocale) {
408
+ options.locale = await promptForLocale(detectSystemLocale());
409
+ } else {
410
+ options.locale = await maybePromptForLocale(globalLocale, { promptMode: "always" });
411
+ }
412
+ if (!globalLocale) {
413
+ await runtimeState.ensureGlobalLocale({ preferredLocale: options.locale, interactive: false });
414
+ }
415
+ setLocale(options.locale || config.DEFAULT_LOCALE);
416
+
417
+ const result = initProject(process.cwd(), options);
418
+
419
+ if (options.withOpera) {
420
+ const opera = require("./opera");
421
+ await opera.install(result.root, {
422
+ locale: options.locale,
423
+ bootstrap: !options.noBootstrap,
424
+ bootstrapMode: options.bootstrapMode,
425
+ technicalLevel: options.technicalLevel,
426
+ projectState: options.projectState,
427
+ docsState: options.docsState,
428
+ decisionOwnership: options.decisionOwnership,
429
+ });
430
+ }
431
+ }
432
+
433
+ module.exports = { initProject, cmdInit, buildDefaultControl };