trackops 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/env.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("fs");
4
- const path = require("path");
5
- const config = require("./config");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const config = require("./config");
6
+ const { t, setLocale } = require("./i18n");
6
7
 
7
8
  const SERVICE_ENV_KEYS = {
8
9
  OpenAI: ["OPENAI_API_KEY"],
@@ -184,7 +185,7 @@ function syncEnvironment(contextOrRoot, controlState, options = {}) {
184
185
  return auditEnvironment(context, control);
185
186
  }
186
187
 
187
- function auditEnvironment(contextOrRoot, controlState) {
188
+ function auditEnvironment(contextOrRoot, controlState) {
188
189
  const context = config.ensureContext(contextOrRoot);
189
190
  const control = controlState || config.loadControl(context);
190
191
  const envMeta = normalizeEnvironmentMeta(control, context);
@@ -206,30 +207,41 @@ function auditEnvironment(contextOrRoot, controlState) {
206
207
  presentKeys,
207
208
  missingKeys,
208
209
  lastAuditAt: envMeta.lastAuditAt,
209
- };
210
- }
211
-
212
- function cmdStatus(contextOrRoot) {
213
- const audit = auditEnvironment(contextOrRoot);
214
- console.log("Environment:");
215
- console.log(` Root .env: ${audit.files.rootEnv}`);
216
- console.log(` Example: ${audit.files.rootExample}`);
217
- console.log(` App bridge: ${audit.files.appBridge}`);
218
- console.log(` Bridge mode: ${audit.bridgeMode}`);
219
- console.log(` Required keys: ${audit.requiredKeys.length ? audit.requiredKeys.join(", ") : "none"}`);
220
- console.log(` Present: ${audit.presentKeys.length ? audit.presentKeys.join(", ") : "none"}`);
221
- console.log(` Missing: ${audit.missingKeys.length ? audit.missingKeys.join(", ") : "none"}`);
222
- }
223
-
224
- function cmdSync(contextOrRoot) {
225
- const context = config.ensureContext(contextOrRoot);
226
- const control = config.loadControl(context);
227
- const audit = syncEnvironment(context, control);
228
- console.log(`Environment synced at ${path.relative(context.workspaceRoot, context.env.rootFile)}`);
229
- if (audit.missingKeys.length) {
230
- console.log(`Missing required keys: ${audit.missingKeys.join(", ")}`);
231
- }
232
- }
210
+ };
211
+ }
212
+
213
+ function initLocale(contextOrRoot) {
214
+ try {
215
+ const control = config.loadControl(config.ensureContext(contextOrRoot));
216
+ setLocale(config.getLocale(control));
217
+ } catch (_error) {
218
+ setLocale("es");
219
+ }
220
+ }
221
+
222
+ function cmdStatus(contextOrRoot) {
223
+ initLocale(contextOrRoot);
224
+ const audit = auditEnvironment(contextOrRoot);
225
+ console.log(t("env.status.title"));
226
+ console.log(t("env.status.rootEnv", { path: audit.files.rootEnv }));
227
+ console.log(t("env.status.example", { path: audit.files.rootExample }));
228
+ console.log(t("env.status.appBridge", { path: audit.files.appBridge }));
229
+ console.log(t("env.status.bridgeMode", { value: audit.bridgeMode }));
230
+ console.log(t("env.status.required", { value: audit.requiredKeys.length ? audit.requiredKeys.join(", ") : t("locale.none") }));
231
+ console.log(t("env.status.present", { value: audit.presentKeys.length ? audit.presentKeys.join(", ") : t("locale.none") }));
232
+ console.log(t("env.status.missing", { value: audit.missingKeys.length ? audit.missingKeys.join(", ") : t("locale.none") }));
233
+ }
234
+
235
+ function cmdSync(contextOrRoot) {
236
+ initLocale(contextOrRoot);
237
+ const context = config.ensureContext(contextOrRoot);
238
+ const control = config.loadControl(context);
239
+ const audit = syncEnvironment(context, control);
240
+ console.log(t("env.sync.updated", { path: path.relative(context.workspaceRoot, context.env.rootFile) }));
241
+ if (audit.missingKeys.length) {
242
+ console.log(t("env.sync.missing", { value: audit.missingKeys.join(", ") }));
243
+ }
244
+ }
233
245
 
234
246
  module.exports = {
235
247
  SERVICE_ENV_KEYS,
package/lib/i18n.js CHANGED
@@ -38,10 +38,11 @@ function getLocale() {
38
38
 
39
39
  function t(key, params) {
40
40
  ensureLoaded();
41
- let message =
42
- currentMessages[key] ||
43
- (fallbackMessages && fallbackMessages[key]) ||
44
- key;
41
+ const found = currentMessages[key] || (fallbackMessages && fallbackMessages[key]);
42
+ let message = found || key;
43
+ if (!found && process.env.NODE_ENV !== "production" && process.env.TRACKOPS_DEBUG) {
44
+ process.stderr.write(`[i18n] Missing key: ${key}\n`);
45
+ }
45
46
  if (params) {
46
47
  message = message.replace(/\{(\w+)\}/g, (match, paramKey) =>
47
48
  params[paramKey] !== undefined ? String(params[paramKey]) : match
package/lib/init.js CHANGED
@@ -12,15 +12,36 @@ const { t, setLocale } = require("./i18n");
12
12
  const { detectSystemLocale, promptForLocale, maybePromptForLocale, resolveLocale } = require("./locale");
13
13
  const runtimeState = require("./runtime-state");
14
14
 
15
- const GENERATED_SCRIPT_COMMANDS = {
15
+ const GENERATED_SCRIPT_COMMANDS = {
16
16
  ops: "npx --yes trackops",
17
17
  "ops:help": "npx --yes trackops help",
18
18
  "ops:dashboard": "npx --yes trackops dashboard",
19
19
  "ops:status": "npx --yes trackops status",
20
20
  "ops:next": "npx --yes trackops next",
21
21
  "ops:sync": "npx --yes trackops sync",
22
- "ops:repo": "npx --yes trackops refresh-repo",
23
- };
22
+ "ops:repo": "npx --yes trackops refresh-repo",
23
+ };
24
+
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
+ ]);
24
45
 
25
46
  function nowIso() {
26
47
  return new Date().toISOString();
@@ -81,7 +102,7 @@ function buildDefaultControl(context, options) {
81
102
  return {
82
103
  meta: {
83
104
  projectName: options.name || "My Project",
84
- controlVersion: 2,
105
+ controlVersion: config.DEFAULT_CONTROL_VERSION,
85
106
  locale,
86
107
  phases,
87
108
  updatedAt: nowIso(),
@@ -112,12 +133,31 @@ function buildDefaultControl(context, options) {
112
133
  explanationMode: null,
113
134
  capturedAt: null,
114
135
  },
115
- discovery: {
116
- projectState: null,
117
- documentationState: null,
118
- availableArtifacts: [],
119
- },
120
- },
136
+ discovery: {
137
+ projectState: null,
138
+ documentationState: null,
139
+ availableArtifacts: [],
140
+ },
141
+ quality: {
142
+ baselineProfile: "baseline",
143
+ activeProfiles: [],
144
+ verification: {
145
+ testCommands: [],
146
+ buildCommands: [],
147
+ smokeCommands: [],
148
+ reviewRequired: false,
149
+ },
150
+ lastReportAt: null,
151
+ lastVerificationAt: null,
152
+ lastReleaseReadiness: null,
153
+ lastPromotionReadiness: null,
154
+ },
155
+ agentInbox: {
156
+ pending: [],
157
+ history: [],
158
+ lastIssuedAt: null,
159
+ },
160
+ },
121
161
  checks: {
122
162
  lastBuild: { status: "pending", date: null, note: "" },
123
163
  lastTest: { status: "pending", date: null, note: "" },
@@ -132,19 +172,30 @@ function buildDefaultControl(context, options) {
132
172
  milestones: [],
133
173
  decisionsPending: [],
134
174
  tasks: [
135
- {
136
- id: "ops-bootstrap",
137
- title: t("init.defaultTaskTitle"),
175
+ {
176
+ id: "ops-bootstrap",
177
+ title: t("init.defaultTaskTitle"),
138
178
  phase: phases[0]?.id || "O",
139
179
  stream: "Operations",
140
180
  priority: "P0",
141
181
  status: "pending",
142
182
  required: true,
143
183
  dependsOn: [],
144
- summary: t("init.defaultTaskSummary"),
145
- acceptance: [],
146
- history: [{ at: nowIso(), action: "create", note: "trackops init" }],
147
- },
184
+ summary: t("init.defaultTaskSummary"),
185
+ acceptance: [],
186
+ execution: {
187
+ owner: config.DEFAULT_EXECUTION_OWNER,
188
+ lastActor: "system",
189
+ lastSource: "trackops_init",
190
+ currentSessionId: null,
191
+ lastSessionId: null,
192
+ lastSessionStatus: null,
193
+ awaitingUserConfirmation: false,
194
+ verificationPending: false,
195
+ updatedAt: nowIso(),
196
+ },
197
+ history: [{ at: nowIso(), action: "create", note: "trackops init" }],
198
+ },
148
199
  ],
149
200
  findings: [],
150
201
  };
@@ -176,41 +227,117 @@ function installHooks(context) {
176
227
 
177
228
  if (fs.existsSync(path.join(context.workspaceRoot, ".git"))) {
178
229
  const hooksPath = context.layout === "split" ? "ops/.githooks" : ".githooks";
179
- spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
230
+ const result = spawnSync("git", ["config", "core.hooksPath", hooksPath], { cwd: context.workspaceRoot, encoding: "utf8" });
231
+ if (result.status !== 0) {
232
+ console.log(t("cli.hooksError"));
233
+ }
180
234
  }
181
235
  }
182
236
 
183
- function ensureTmpDir(context) {
237
+ function ensureTmpDir(context) {
184
238
  fs.mkdirSync(context.paths.tmpDir, { recursive: true });
185
239
  const gitkeep = path.join(context.paths.tmpDir, ".gitkeep");
186
240
  if (!fs.existsSync(gitkeep)) {
187
241
  fs.writeFileSync(gitkeep, "", "utf8");
188
- }
189
- }
190
-
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.");
196
- }
197
- }
198
-
199
- function initSplitProject(root, options) {
200
- const targetRoot = path.resolve(root);
201
- fs.mkdirSync(targetRoot, { recursive: true });
202
- ensureSplitLayoutAllowed(targetRoot);
203
-
204
- const manifest = workspace.buildManifest();
205
- const context = config.createSplitContext(targetRoot, manifest);
206
- fs.mkdirSync(context.appRoot, { recursive: true });
207
- fs.mkdirSync(context.opsRoot, { recursive: true });
208
- config.saveWorkspaceManifest(context, manifest);
209
- workspace.ensureRootGitignore(targetRoot);
210
-
211
- const control = buildDefaultControl(context, options);
212
- control.meta.projectName = options.name || detectProjectName(context.workspaceRoot);
213
- config.saveControl(context, control);
242
+ }
243
+ }
244
+
245
+ function isRetryableMoveError(error) {
246
+ return ["EPERM", "EXDEV", "EBUSY", "ENOTEMPTY"].includes(error?.code);
247
+ }
248
+
249
+ function moveEntry(fromPath, toPath) {
250
+ if (!fs.existsSync(fromPath)) return;
251
+ fs.mkdirSync(path.dirname(toPath), { recursive: true });
252
+ try {
253
+ fs.renameSync(fromPath, toPath);
254
+ } catch (error) {
255
+ if (!isRetryableMoveError(error)) throw error;
256
+ const stat = fs.statSync(fromPath);
257
+ if (stat.isDirectory()) {
258
+ fs.cpSync(fromPath, toPath, { recursive: true, force: true });
259
+ fs.rmSync(fromPath, { recursive: true, force: true });
260
+ return;
261
+ }
262
+ fs.copyFileSync(fromPath, toPath);
263
+ fs.rmSync(fromPath, { force: true });
264
+ }
265
+ }
266
+
267
+ function collectRootEntries(targetRoot) {
268
+ return fs.readdirSync(targetRoot, { withFileTypes: true })
269
+ .filter((entry) => ![".", ".."].includes(entry.name));
270
+ }
271
+
272
+ function analyzeSplitInit(targetRoot) {
273
+ const entries = collectRootEntries(targetRoot);
274
+ const manifestPath = path.join(targetRoot, config.WORKSPACE_MANIFEST);
275
+ const legacyControlPath = path.join(targetRoot, "project_control.json");
276
+ const hasManifest = fs.existsSync(manifestPath);
277
+
278
+ if (hasManifest) {
279
+ return { mode: "upgrade", entries };
280
+ }
281
+
282
+ if (fs.existsSync(legacyControlPath)) {
283
+ throw new Error(t("init.error.legacyDetected"));
284
+ }
285
+
286
+ const conflicts = entries
287
+ .map((entry) => entry.name)
288
+ .filter((name) => [config.DEFAULT_APP_DIR, config.DEFAULT_OPS_DIR, config.WORKSPACE_MANIFEST].includes(name));
289
+
290
+ if (conflicts.length) {
291
+ throw new Error(t("init.error.reservedConflict", { entries: conflicts.join(", ") }));
292
+ }
293
+
294
+ const movableEntries = entries.filter((entry) => !ROOT_PRESERVED_ENTRIES.has(entry.name));
295
+ return {
296
+ mode: movableEntries.length ? "adopt" : "new",
297
+ entries,
298
+ movableEntries,
299
+ };
300
+ }
301
+
302
+ function adoptExistingProject(targetRoot, appRoot, movableEntries) {
303
+ for (const entry of movableEntries) {
304
+ moveEntry(path.join(targetRoot, entry.name), path.join(appRoot, entry.name));
305
+ }
306
+ }
307
+
308
+ function initSplitProject(root, options) {
309
+ const targetRoot = path.resolve(root);
310
+ fs.mkdirSync(targetRoot, { recursive: true });
311
+ const projectName = options.name || detectProjectName(targetRoot);
312
+ const initMode = analyzeSplitInit(targetRoot);
313
+ const manifest = workspace.buildManifest();
314
+ const context = config.createSplitContext(targetRoot, manifest);
315
+
316
+ if (initMode.mode === "adopt") {
317
+ fs.mkdirSync(context.appRoot, { recursive: true });
318
+ adoptExistingProject(targetRoot, context.appRoot, initMode.movableEntries);
319
+ }
320
+
321
+ fs.mkdirSync(context.appRoot, { recursive: true });
322
+ fs.mkdirSync(context.opsRoot, { recursive: true });
323
+ config.saveWorkspaceManifest(context, manifest);
324
+ workspace.ensureRootGitignore(targetRoot);
325
+
326
+ const controlFile = context.controlFile || path.join(context.opsRoot, "project_control.json");
327
+ const isUpgrade = initMode.mode === "upgrade" && fs.existsSync(controlFile);
328
+ let control;
329
+ if (isUpgrade) {
330
+ control = JSON.parse(fs.readFileSync(controlFile, "utf8"));
331
+ if (!control.meta.phases) control.meta.phases = options.phases || config.buildDefaultPhases(options.locale);
332
+ if (!control.meta.locale) control.meta.locale = options.locale || config.DEFAULT_LOCALE;
333
+ if (!control.meta.controlVersion || control.meta.controlVersion < config.DEFAULT_CONTROL_VERSION) control.meta.controlVersion = config.DEFAULT_CONTROL_VERSION;
334
+ control.meta.updatedAt = nowIso();
335
+ config.saveControl(context, control);
336
+ } else {
337
+ control = buildDefaultControl(context, options);
338
+ control.meta.projectName = projectName;
339
+ config.saveControl(context, control);
340
+ }
214
341
 
215
342
  installHooks(context);
216
343
  ensureTmpDir(context);
@@ -224,16 +351,27 @@ function initSplitProject(root, options) {
224
351
  // ignore
225
352
  }
226
353
 
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
- }
354
+ if (isUpgrade) {
355
+ console.log(t("init.updated", { file: ".trackops-workspace.json" }));
356
+ console.log(t("init.updated", { file: "ops/project_control.json" }));
357
+ console.log(t("init.updated", { file: "ops/.githooks/" }));
358
+ console.log(t("init.updated", { file: ".env" }));
359
+ console.log(t("init.updated", { file: ".env.example" }));
360
+ } else {
361
+ console.log(t("init.created", { file: ".trackops-workspace.json" }));
362
+ console.log(t("init.created", { file: "ops/project_control.json" }));
363
+ console.log(t("init.created", { file: "ops/.githooks/" }));
364
+ console.log(t("init.created", { file: ".env" }));
365
+ console.log(t("init.created", { file: ".env.example" }));
366
+ if (initMode.mode === "adopt") {
367
+ console.log(t("init.adoptedExistingRepo", { dir: path.basename(context.appRoot) }));
368
+ }
369
+ }
370
+ console.log("");
371
+ console.log(t("init.welcome"));
372
+
373
+ return { root: context.workspaceRoot, context, isUpgrade, operaDetected: false };
374
+ }
237
375
 
238
376
  function initLegacyProject(root, options) {
239
377
  const targetRoot = path.resolve(root);
@@ -250,7 +388,7 @@ function initLegacyProject(root, options) {
250
388
  const existing = JSON.parse(fs.readFileSync(controlFile, "utf8"));
251
389
  if (!existing.meta.phases) existing.meta.phases = options.phases || config.buildDefaultPhases(options.locale);
252
390
  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;
391
+ if (!existing.meta.controlVersion || existing.meta.controlVersion < config.DEFAULT_CONTROL_VERSION) existing.meta.controlVersion = config.DEFAULT_CONTROL_VERSION;
254
392
  existing.meta.updatedAt = nowIso();
255
393
  fs.writeFileSync(controlFile, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
256
394
  console.log(t("init.updated", { file: "project_control.json" }));