trackops 1.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.
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/bin/trackops.js +103 -0
- package/lib/config.js +97 -0
- package/lib/control.js +575 -0
- package/lib/i18n.js +53 -0
- package/lib/init.js +200 -0
- package/lib/opera.js +202 -0
- package/lib/registry.js +182 -0
- package/lib/server.js +451 -0
- package/lib/skills.js +159 -0
- package/locales/en.json +142 -0
- package/locales/es.json +142 -0
- package/package.json +46 -0
- package/templates/etapa/agent.md +26 -0
- package/templates/etapa/genesis.md +94 -0
- package/templates/etapa/references/autonomy-and-recovery.md +117 -0
- package/templates/etapa/references/etapa-cycle.md +193 -0
- package/templates/etapa/registry.md +28 -0
- package/templates/etapa/router.md +39 -0
- package/templates/hooks/post-checkout +2 -0
- package/templates/hooks/post-commit +2 -0
- package/templates/hooks/post-merge +2 -0
- package/templates/opera/agent.md +26 -0
- package/templates/opera/genesis.md +94 -0
- package/templates/opera/references/autonomy-and-recovery.md +117 -0
- package/templates/opera/references/opera-cycle.md +193 -0
- package/templates/opera/registry.md +28 -0
- package/templates/opera/router.md +39 -0
- package/templates/skills/changelog-updater/SKILL.md +69 -0
- package/templates/skills/commiter/SKILL.md +99 -0
- package/templates/skills/project-starter-skill/SKILL.md +202 -0
- package/templates/skills/project-starter-skill/references/opera-cycle.md +193 -0
- package/ui/app.js +950 -0
- package/ui/index.html +356 -0
- package/ui/styles.css +688 -0
package/lib/init.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
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 { DEFAULT_PHASES, DEFAULT_LOCALE } = require("./config");
|
|
8
|
+
const registry = require("./registry");
|
|
9
|
+
const { t, setLocale } = require("./i18n");
|
|
10
|
+
|
|
11
|
+
function nowIso() {
|
|
12
|
+
return new Date().toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseArgs(args) {
|
|
16
|
+
const options = { locale: null, name: null, withOpera: false, phases: null };
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
if (args[i] === "--locale" && args[i + 1]) { options.locale = args[i + 1]; i += 1; }
|
|
19
|
+
else if (args[i] === "--name" && args[i + 1]) { options.name = args[i + 1]; i += 1; }
|
|
20
|
+
else if (args[i] === "--with-opera" || args[i] === "--with-etapa") { options.withOpera = true; }
|
|
21
|
+
else if (args[i] === "--phases" && args[i + 1]) {
|
|
22
|
+
try { options.phases = JSON.parse(args[i + 1]); } catch (_e) { /* ignore */ }
|
|
23
|
+
i += 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectProjectName(root) {
|
|
30
|
+
const pkgPath = path.join(root, "package.json");
|
|
31
|
+
if (fs.existsSync(pkgPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
34
|
+
return pkg.displayName || pkg.name || path.basename(root);
|
|
35
|
+
} catch (_e) { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
return path.basename(root);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasExistingOpera(root) {
|
|
41
|
+
return (
|
|
42
|
+
fs.existsSync(path.join(root, ".agent", "hub")) ||
|
|
43
|
+
fs.existsSync(path.join(root, ".agents", "skills"))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildDefaultControl(options) {
|
|
48
|
+
const locale = options.locale || DEFAULT_LOCALE;
|
|
49
|
+
setLocale(locale);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
meta: {
|
|
53
|
+
projectName: options.name || "My Project",
|
|
54
|
+
controlVersion: 2,
|
|
55
|
+
locale,
|
|
56
|
+
phases: options.phases || DEFAULT_PHASES,
|
|
57
|
+
updatedAt: nowIso(),
|
|
58
|
+
executionSource: "project_control.json",
|
|
59
|
+
currentFocus: t("init.defaultFocus"),
|
|
60
|
+
focusPhase: (options.phases || DEFAULT_PHASES)[0]?.id || "E",
|
|
61
|
+
deliveryTarget: t("init.defaultTarget"),
|
|
62
|
+
},
|
|
63
|
+
checks: {
|
|
64
|
+
lastBuild: { status: "pending", date: null, note: "" },
|
|
65
|
+
lastTest: { status: "pending", date: null, note: "" },
|
|
66
|
+
lastReview: { status: "pending", date: null, note: "" },
|
|
67
|
+
},
|
|
68
|
+
rituals: {
|
|
69
|
+
startOfSession: ["trackops status", "trackops next"],
|
|
70
|
+
beforeImplementation: ["trackops task start <task-id>"],
|
|
71
|
+
endOfSession: ["trackops task review|complete|block <task-id> <note>", "trackops sync"],
|
|
72
|
+
beforeCommit: ["trackops status", "trackops sync"],
|
|
73
|
+
},
|
|
74
|
+
milestones: [],
|
|
75
|
+
decisionsPending: [],
|
|
76
|
+
tasks: [
|
|
77
|
+
{
|
|
78
|
+
id: "ops-bootstrap",
|
|
79
|
+
title: t("init.defaultTaskTitle"),
|
|
80
|
+
phase: (options.phases || DEFAULT_PHASES)[0]?.id || "E",
|
|
81
|
+
stream: "Operations",
|
|
82
|
+
priority: "P0",
|
|
83
|
+
status: "pending",
|
|
84
|
+
required: true,
|
|
85
|
+
dependsOn: [],
|
|
86
|
+
summary: t("init.defaultTaskSummary"),
|
|
87
|
+
acceptance: [],
|
|
88
|
+
history: [{ at: nowIso(), action: "create", note: "trackops init" }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
findings: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function upsertScripts(root) {
|
|
96
|
+
const pkgPath = path.join(root, "package.json");
|
|
97
|
+
let pkg = {};
|
|
98
|
+
if (fs.existsSync(pkgPath)) {
|
|
99
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); } catch (_e) { /* ignore */ }
|
|
100
|
+
}
|
|
101
|
+
pkg.scripts = pkg.scripts || {};
|
|
102
|
+
Object.assign(pkg.scripts, {
|
|
103
|
+
ops: "trackops",
|
|
104
|
+
"ops:help": "trackops help",
|
|
105
|
+
"ops:dashboard": "trackops dashboard",
|
|
106
|
+
"ops:status": "trackops status",
|
|
107
|
+
"ops:next": "trackops next",
|
|
108
|
+
"ops:sync": "trackops sync",
|
|
109
|
+
"ops:repo": "trackops refresh-repo",
|
|
110
|
+
});
|
|
111
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function installHooks(root) {
|
|
115
|
+
const hooksDir = path.join(root, ".githooks");
|
|
116
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const hookContent = "#!/bin/sh\nnpx trackops refresh-repo --quiet >/dev/null 2>&1 || true\n";
|
|
119
|
+
for (const hookName of ["post-commit", "post-checkout", "post-merge"]) {
|
|
120
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
121
|
+
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(path.join(root, ".git"))) {
|
|
125
|
+
spawnSync("git", ["config", "core.hooksPath", ".githooks"], { cwd: root, encoding: "utf8" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ensureTmpDir(root) {
|
|
130
|
+
const tmpDir = path.join(root, ".tmp");
|
|
131
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
132
|
+
const gitkeep = path.join(tmpDir, ".gitkeep");
|
|
133
|
+
if (!fs.existsSync(gitkeep)) {
|
|
134
|
+
fs.writeFileSync(gitkeep, "", "utf8");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function initProject(root, options) {
|
|
139
|
+
const targetRoot = path.resolve(root);
|
|
140
|
+
const controlFile = path.join(targetRoot, "project_control.json");
|
|
141
|
+
const isUpgrade = fs.existsSync(controlFile);
|
|
142
|
+
|
|
143
|
+
if (!options.name) {
|
|
144
|
+
options.name = detectProjectName(targetRoot);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (isUpgrade) {
|
|
148
|
+
// Upgrade: add new meta fields if missing
|
|
149
|
+
const existing = JSON.parse(fs.readFileSync(controlFile, "utf8"));
|
|
150
|
+
if (!existing.meta.phases) existing.meta.phases = options.phases || DEFAULT_PHASES;
|
|
151
|
+
if (!existing.meta.locale) existing.meta.locale = options.locale || DEFAULT_LOCALE;
|
|
152
|
+
if (!existing.meta.controlVersion || existing.meta.controlVersion < 2) existing.meta.controlVersion = 2;
|
|
153
|
+
existing.meta.updatedAt = nowIso();
|
|
154
|
+
fs.writeFileSync(controlFile, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
|
|
155
|
+
console.log(t("init.updated", { file: "project_control.json" }));
|
|
156
|
+
} else {
|
|
157
|
+
const control = buildDefaultControl(options);
|
|
158
|
+
fs.writeFileSync(controlFile, `${JSON.stringify(control, null, 2)}\n`, "utf8");
|
|
159
|
+
console.log(t("init.created", { file: "project_control.json" }));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
upsertScripts(targetRoot);
|
|
163
|
+
console.log(t("init.updated", { file: "package.json" }));
|
|
164
|
+
|
|
165
|
+
installHooks(targetRoot);
|
|
166
|
+
console.log(t("init.created", { file: ".githooks/" }));
|
|
167
|
+
|
|
168
|
+
ensureTmpDir(targetRoot);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
registry.registerProject(targetRoot);
|
|
172
|
+
console.log(t("init.registered"));
|
|
173
|
+
} catch (_e) { /* ignore */ }
|
|
174
|
+
|
|
175
|
+
const operaDetected = hasExistingOpera(targetRoot);
|
|
176
|
+
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log(t("init.welcome"));
|
|
179
|
+
|
|
180
|
+
return { root: targetRoot, isUpgrade, operaDetected };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function cmdInit(args) {
|
|
184
|
+
const options = parseArgs(args || []);
|
|
185
|
+
const root = process.cwd();
|
|
186
|
+
setLocale(options.locale || DEFAULT_LOCALE);
|
|
187
|
+
|
|
188
|
+
const result = initProject(root, options);
|
|
189
|
+
|
|
190
|
+
if (options.withOpera) {
|
|
191
|
+
const opera = require("./opera");
|
|
192
|
+
opera.install(root, {});
|
|
193
|
+
} else if (result.operaDetected) {
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log(t("opera.primitiveDetected"));
|
|
196
|
+
console.log(" Run 'trackops opera install' to upgrade to managed OPERA.");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { initProject, cmdInit, buildDefaultControl };
|
package/lib/opera.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const config = require("./config");
|
|
7
|
+
const { t, setLocale } = require("./i18n");
|
|
8
|
+
|
|
9
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates", "opera");
|
|
10
|
+
const SKILLS_TEMPLATES_DIR = path.join(__dirname, "..", "templates", "skills");
|
|
11
|
+
const OPERA_VERSION = require("../package.json").version;
|
|
12
|
+
|
|
13
|
+
function nowIso() {
|
|
14
|
+
return new Date().toISOString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function copyTemplate(templatePath, targetPath, replacements) {
|
|
18
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
19
|
+
let content = fs.readFileSync(templatePath, "utf8");
|
|
20
|
+
if (replacements) {
|
|
21
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
22
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function copyDirRecursive(src, dest) {
|
|
29
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
30
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
31
|
+
const srcPath = path.join(src, entry.name);
|
|
32
|
+
const destPath = path.join(dest, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
copyDirRecursive(srcPath, destPath);
|
|
35
|
+
} else {
|
|
36
|
+
fs.copyFileSync(srcPath, destPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function install(root, options) {
|
|
42
|
+
const controlFile = config.controlFilePath(root);
|
|
43
|
+
if (!fs.existsSync(controlFile)) {
|
|
44
|
+
throw new Error("project_control.json not found. Run 'trackops init' first.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const control = config.loadControl(root);
|
|
48
|
+
setLocale(config.getLocale(control));
|
|
49
|
+
|
|
50
|
+
if (config.isOperaInstalled(control)) {
|
|
51
|
+
console.log(t("opera.alreadyInstalled", { version: config.getOperaVersion(control) }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectName = control.meta.projectName || "Project";
|
|
56
|
+
|
|
57
|
+
// Create .agent/hub/ with agent.md and router.md
|
|
58
|
+
const agentHubDir = path.join(root, ".agent", "hub");
|
|
59
|
+
fs.mkdirSync(agentHubDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const agentTemplatePath = path.join(TEMPLATES_DIR, "agent.md");
|
|
62
|
+
const routerTemplatePath = path.join(TEMPLATES_DIR, "router.md");
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(agentTemplatePath) && !fs.existsSync(path.join(agentHubDir, "agent.md"))) {
|
|
65
|
+
copyTemplate(agentTemplatePath, path.join(agentHubDir, "agent.md"), { PROJECT_NAME: projectName });
|
|
66
|
+
}
|
|
67
|
+
if (fs.existsSync(routerTemplatePath) && !fs.existsSync(path.join(agentHubDir, "router.md"))) {
|
|
68
|
+
copyTemplate(routerTemplatePath, path.join(agentHubDir, "router.md"), { PROJECT_NAME: projectName });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create .agent/skills/_registry.md
|
|
72
|
+
const skillsRegistryDir = path.join(root, ".agent", "skills");
|
|
73
|
+
fs.mkdirSync(skillsRegistryDir, { recursive: true });
|
|
74
|
+
const registryPath = path.join(skillsRegistryDir, "_registry.md");
|
|
75
|
+
const registryTemplatePath = path.join(TEMPLATES_DIR, "registry.md");
|
|
76
|
+
if (fs.existsSync(registryTemplatePath) && !fs.existsSync(registryPath)) {
|
|
77
|
+
copyTemplate(registryTemplatePath, registryPath, { PROJECT_NAME: projectName });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create genesis.md if not exists
|
|
81
|
+
const genesisTemplatePath = path.join(TEMPLATES_DIR, "genesis.md");
|
|
82
|
+
const genesisPath = path.join(root, "genesis.md");
|
|
83
|
+
if (fs.existsSync(genesisTemplatePath) && !fs.existsSync(genesisPath)) {
|
|
84
|
+
copyTemplate(genesisTemplatePath, genesisPath, { PROJECT_NAME: projectName });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Copy OPERA references
|
|
88
|
+
const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
|
|
89
|
+
if (fs.existsSync(refsTemplateDir)) {
|
|
90
|
+
const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
|
|
91
|
+
copyDirRecursive(refsTemplateDir, starterSkillDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Install project-starter-skill SKILL.md
|
|
95
|
+
const starterSkillTemplate = path.join(SKILLS_TEMPLATES_DIR, "project-starter-skill", "SKILL.md");
|
|
96
|
+
const starterSkillTarget = path.join(root, ".agents", "skills", "project-starter-skill", "SKILL.md");
|
|
97
|
+
if (fs.existsSync(starterSkillTemplate) && !fs.existsSync(starterSkillTarget)) {
|
|
98
|
+
fs.mkdirSync(path.dirname(starterSkillTarget), { recursive: true });
|
|
99
|
+
fs.copyFileSync(starterSkillTemplate, starterSkillTarget);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Mark as installed in control
|
|
103
|
+
control.meta.opera = {
|
|
104
|
+
installed: true,
|
|
105
|
+
version: OPERA_VERSION,
|
|
106
|
+
installedAt: nowIso(),
|
|
107
|
+
skills: [],
|
|
108
|
+
};
|
|
109
|
+
config.saveControl(root, control);
|
|
110
|
+
|
|
111
|
+
console.log(t("opera.installed", { version: OPERA_VERSION }));
|
|
112
|
+
|
|
113
|
+
// Auto-install base skills (commiter, changelog-updater)
|
|
114
|
+
const skills = require("./skills");
|
|
115
|
+
for (const skillName of ["commiter", "changelog-updater"]) {
|
|
116
|
+
try { skills.installSkill(root, skillName); }
|
|
117
|
+
catch (_e) { /* already installed or not in catalog — skip silently */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function status(root) {
|
|
122
|
+
const control = config.loadControl(root);
|
|
123
|
+
setLocale(config.getLocale(control));
|
|
124
|
+
|
|
125
|
+
if (!config.isOperaInstalled(control)) {
|
|
126
|
+
console.log(t("opera.notInstalled"));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const opera = control.meta.opera;
|
|
131
|
+
console.log(`OPERA v${opera.version}`);
|
|
132
|
+
console.log(` Installed: ${opera.installedAt}`);
|
|
133
|
+
console.log(` Skills: ${(opera.skills || []).join(", ") || "none"}`);
|
|
134
|
+
|
|
135
|
+
// Check structural integrity
|
|
136
|
+
const checks = [
|
|
137
|
+
[".agent/hub/agent.md", fs.existsSync(path.join(root, ".agent", "hub", "agent.md"))],
|
|
138
|
+
[".agent/hub/router.md", fs.existsSync(path.join(root, ".agent", "hub", "router.md"))],
|
|
139
|
+
[".agent/skills/_registry.md", fs.existsSync(path.join(root, ".agent", "skills", "_registry.md"))],
|
|
140
|
+
["genesis.md", fs.existsSync(path.join(root, "genesis.md"))],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
console.log(" Structure:");
|
|
144
|
+
for (const [file, exists] of checks) {
|
|
145
|
+
console.log(` ${exists ? "\u2705" : "\u274C"} ${file}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function configure(root, args) {
|
|
150
|
+
const control = config.loadControl(root);
|
|
151
|
+
setLocale(config.getLocale(control));
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
154
|
+
if (args[i] === "--locale" && args[i + 1]) {
|
|
155
|
+
control.meta.locale = args[i + 1];
|
|
156
|
+
i += 1;
|
|
157
|
+
}
|
|
158
|
+
if (args[i] === "--phases" && args[i + 1]) {
|
|
159
|
+
try {
|
|
160
|
+
control.meta.phases = JSON.parse(args[i + 1]);
|
|
161
|
+
} catch (_e) {
|
|
162
|
+
console.error("Invalid phases JSON.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
i += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
config.saveControl(root, control);
|
|
170
|
+
console.log("Configuration updated.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function upgrade(root) {
|
|
174
|
+
const control = config.loadControl(root);
|
|
175
|
+
setLocale(config.getLocale(control));
|
|
176
|
+
|
|
177
|
+
if (!config.isOperaInstalled(control)) {
|
|
178
|
+
console.log(t("opera.notInstalled"));
|
|
179
|
+
console.log("Run 'trackops opera install' first.");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Re-copy reference files
|
|
184
|
+
const refsTemplateDir = path.join(TEMPLATES_DIR, "references");
|
|
185
|
+
if (fs.existsSync(refsTemplateDir)) {
|
|
186
|
+
const starterSkillDir = path.join(root, ".agents", "skills", "project-starter-skill", "references");
|
|
187
|
+
copyDirRecursive(refsTemplateDir, starterSkillDir);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
control.meta.opera.version = OPERA_VERSION;
|
|
191
|
+
config.saveControl(root, control);
|
|
192
|
+
console.log(t("opera.upgraded", { version: OPERA_VERSION }));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ── CLI commands ── */
|
|
196
|
+
|
|
197
|
+
function cmdInstall(root, args) { install(root, {}); }
|
|
198
|
+
function cmdStatus(root) { status(root); }
|
|
199
|
+
function cmdConfigure(root, args) { configure(root, args); }
|
|
200
|
+
function cmdUpgrade(root) { upgrade(root); }
|
|
201
|
+
|
|
202
|
+
module.exports = { install, status, configure, upgrade, cmdInstall, cmdStatus, cmdConfigure, cmdUpgrade };
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const { t } = require("./i18n");
|
|
8
|
+
|
|
9
|
+
const REGISTRY_DIR = path.join(os.homedir(), ".codex", "trackops");
|
|
10
|
+
const REGISTRY_FILE = path.join(REGISTRY_DIR, "registry.json");
|
|
11
|
+
|
|
12
|
+
function nowIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function slugify(value) {
|
|
17
|
+
return String(value || "")
|
|
18
|
+
.normalize("NFD")
|
|
19
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "")
|
|
23
|
+
.slice(0, 48);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shortHash(value) {
|
|
27
|
+
let hash = 0;
|
|
28
|
+
const input = String(value || "");
|
|
29
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
30
|
+
hash = (hash * 31 + input.charCodeAt(index)) >>> 0;
|
|
31
|
+
}
|
|
32
|
+
return hash.toString(36).slice(0, 6);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureRegistryDir() {
|
|
36
|
+
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadRegistry() {
|
|
40
|
+
ensureRegistryDir();
|
|
41
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
42
|
+
return { version: 1, updatedAt: nowIso(), projects: [] };
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveRegistry(registry) {
|
|
48
|
+
ensureRegistryDir();
|
|
49
|
+
registry.updatedAt = nowIso();
|
|
50
|
+
fs.writeFileSync(REGISTRY_FILE, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isProjectInstalled(rootDir) {
|
|
54
|
+
return fs.existsSync(path.join(rootDir, "project_control.json"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function inspectProject(rootDir) {
|
|
58
|
+
const root = path.resolve(rootDir);
|
|
59
|
+
const controlStateFile = path.join(root, "project_control.json");
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(controlStateFile)) {
|
|
62
|
+
throw new Error(`Project '${root}' does not have trackops installed.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let packageJson = {};
|
|
66
|
+
const packageFile = path.join(root, "package.json");
|
|
67
|
+
if (fs.existsSync(packageFile)) {
|
|
68
|
+
packageJson = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const controlState = JSON.parse(fs.readFileSync(controlStateFile, "utf8"));
|
|
72
|
+
const projectName =
|
|
73
|
+
controlState.meta?.projectName || packageJson.displayName || packageJson.name || path.basename(root);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: `${slugify(projectName) || "project"}-${shortHash(root)}`,
|
|
77
|
+
name: projectName,
|
|
78
|
+
root,
|
|
79
|
+
packageName: packageJson.name || null,
|
|
80
|
+
controlStateFile,
|
|
81
|
+
registeredAt: nowIso(),
|
|
82
|
+
lastSeenAt: nowIso(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function registerProject(rootDir) {
|
|
87
|
+
const registry = loadRegistry();
|
|
88
|
+
const entry = inspectProject(rootDir);
|
|
89
|
+
const existingIndex = registry.projects.findIndex((p) => p.root === entry.root);
|
|
90
|
+
|
|
91
|
+
if (existingIndex >= 0) {
|
|
92
|
+
registry.projects[existingIndex] = {
|
|
93
|
+
...registry.projects[existingIndex],
|
|
94
|
+
...entry,
|
|
95
|
+
registeredAt: registry.projects[existingIndex].registeredAt || entry.registeredAt,
|
|
96
|
+
lastSeenAt: nowIso(),
|
|
97
|
+
};
|
|
98
|
+
} else {
|
|
99
|
+
registry.projects.push(entry);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
registry.projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
+
saveRegistry(registry);
|
|
104
|
+
return registry.projects.find((p) => p.root === entry.root);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unregisterProject(rootDir) {
|
|
108
|
+
const root = path.resolve(rootDir);
|
|
109
|
+
const registry = loadRegistry();
|
|
110
|
+
registry.projects = registry.projects.filter((p) => p.root !== root);
|
|
111
|
+
saveRegistry(registry);
|
|
112
|
+
return registry;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function listProjects() {
|
|
116
|
+
const registry = loadRegistry();
|
|
117
|
+
return registry.projects.map((project) => {
|
|
118
|
+
const available = isProjectInstalled(project.root);
|
|
119
|
+
return { ...project, available };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveProject(projectRef, fallbackRoot) {
|
|
124
|
+
const projects = listProjects();
|
|
125
|
+
|
|
126
|
+
if (!projectRef) {
|
|
127
|
+
if (fallbackRoot) {
|
|
128
|
+
return projects.find((p) => p.root === path.resolve(fallbackRoot)) || null;
|
|
129
|
+
}
|
|
130
|
+
return projects[0] || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const normalizedRef = path.resolve(projectRef);
|
|
134
|
+
return (
|
|
135
|
+
projects.find((p) => p.id === projectRef) ||
|
|
136
|
+
projects.find((p) => p.root === normalizedRef) ||
|
|
137
|
+
null
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ── CLI commands ── */
|
|
142
|
+
|
|
143
|
+
function cmdRegister(root) {
|
|
144
|
+
const project = registerProject(root);
|
|
145
|
+
console.log(`${t("init.registered")} ${project.name}`);
|
|
146
|
+
console.log(project.root);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function cmdList() {
|
|
150
|
+
const projects = listProjects();
|
|
151
|
+
if (!projects.length) {
|
|
152
|
+
console.log("No registered projects.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
projects.forEach((project, index) => {
|
|
156
|
+
console.log(`${index + 1}. ${project.name}`);
|
|
157
|
+
console.log(` id: ${project.id}`);
|
|
158
|
+
console.log(` root: ${project.root}`);
|
|
159
|
+
console.log(` available: ${project.available ? "yes" : "no"}`);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cmdWhere() {
|
|
164
|
+
console.log(REGISTRY_FILE);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
REGISTRY_DIR,
|
|
169
|
+
REGISTRY_FILE,
|
|
170
|
+
inspectProject,
|
|
171
|
+
isProjectInstalled,
|
|
172
|
+
listProjects,
|
|
173
|
+
loadRegistry,
|
|
174
|
+
registerProject,
|
|
175
|
+
resolveProject,
|
|
176
|
+
saveRegistry,
|
|
177
|
+
slugify,
|
|
178
|
+
unregisterProject,
|
|
179
|
+
cmdRegister,
|
|
180
|
+
cmdList,
|
|
181
|
+
cmdWhere,
|
|
182
|
+
};
|