arkaos 4.34.0 → 4.36.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/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/bin/arka-menubar.py +460 -0
- package/config/install-profiles.json +75 -0
- package/core/runtime/opencode.py +252 -0
- package/core/runtime/registry.py +6 -1
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +12 -0
- package/harness/opencode/agents/arka-brand-director-valentina.md +12 -0
- package/harness/opencode/agents/arka-cfo-helena.md +12 -0
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +12 -0
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +12 -0
- package/harness/opencode/agents/arka-content-strategist-rafael.md +12 -0
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +12 -0
- package/harness/opencode/agents/arka-coo-sofia.md +12 -0
- package/harness/opencode/agents/arka-copy-director-eduardo.md +12 -0
- package/harness/opencode/agents/arka-cqo-marta.md +12 -0
- package/harness/opencode/agents/arka-cto-marco.md +12 -0
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +12 -0
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +12 -0
- package/harness/opencode/agents/arka-knowledge-director-clara.md +12 -0
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +12 -0
- package/harness/opencode/agents/arka-marketing-director-luna.md +12 -0
- package/harness/opencode/agents/arka-ops-lead-daniel.md +12 -0
- package/harness/opencode/agents/arka-pm-director-carolina.md +12 -0
- package/harness/opencode/agents/arka-revops-lead-vicente.md +12 -0
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +12 -0
- package/harness/opencode/agents/arka-sales-director-miguel.md +12 -0
- package/harness/opencode/agents/arka-strategy-director-tomas.md +12 -0
- package/harness/opencode/agents/arka-tech-director-francisca.md +12 -0
- package/harness/opencode/agents/arka-tech-lead-paulo.md +12 -0
- package/harness/opencode/agents/arka-video-producer-simao.md +12 -0
- package/harness/opencode/commands/arka-brand.md +9 -0
- package/harness/opencode/commands/arka-community.md +9 -0
- package/harness/opencode/commands/arka-content.md +9 -0
- package/harness/opencode/commands/arka-dev.md +9 -0
- package/harness/opencode/commands/arka-ecom.md +9 -0
- package/harness/opencode/commands/arka-fin.md +9 -0
- package/harness/opencode/commands/arka-kb.md +9 -0
- package/harness/opencode/commands/arka-landing.md +9 -0
- package/harness/opencode/commands/arka-lead.md +9 -0
- package/harness/opencode/commands/arka-mkt.md +9 -0
- package/harness/opencode/commands/arka-ops.md +9 -0
- package/harness/opencode/commands/arka-org.md +9 -0
- package/harness/opencode/commands/arka-pm.md +9 -0
- package/harness/opencode/commands/arka-saas.md +9 -0
- package/harness/opencode/commands/arka-sales.md +9 -0
- package/harness/opencode/commands/arka-strat.md +9 -0
- package/harness/opencode/opencode.json +16 -0
- package/harness/zed/.rules +1 -1
- package/installer/adapters/opencode.js +118 -0
- package/installer/cli.js +31 -1
- package/installer/detect-runtime.js +18 -0
- package/installer/doctor.js +202 -2
- package/installer/index.js +174 -88
- package/installer/menubar.js +262 -0
- package/installer/product-stats.js +93 -0
- package/installer/profile.js +75 -0
- package/installer/prompts.js +303 -41
- package/installer/services.js +487 -0
- package/installer/ui.js +190 -0
- package/installer/update.js +212 -95
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +6 -1
- package/pyproject.toml +1 -1
- package/scripts/harness_gen.py +98 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArkaOS menu bar launcher wiring (Foundation PR-5).
|
|
3
|
+
*
|
|
4
|
+
* Deploys bin/arka-menubar.py to the purge-proof ~/.arkaos/bin/ (the
|
|
5
|
+
* scheduler-daemon.py pattern — SNAPSHOT_DIRS does not ship bin/, and a
|
|
6
|
+
* launchd unit anchored at an npx cache dies on `npm cache clean`; the
|
|
7
|
+
* PR-1 QG blocker) and manages the io.wizardingcode.arkaos.menubar
|
|
8
|
+
* LaunchAgent.
|
|
9
|
+
*
|
|
10
|
+
* macOS-only by design (rumps is a macOS menu bar framework); every
|
|
11
|
+
* other platform reports { action: "unsupported" } gracefully. Default-on
|
|
12
|
+
* (operator decision, PR-5 Phase 0 — same posture as the PR-1 updater)
|
|
13
|
+
* with a persisted opt-out marker so `npx arkaos menubar disable`
|
|
14
|
+
* survives future updates. ESM, Node + Bun, no interactive prompts.
|
|
15
|
+
* Unit generation (`unitFor`) is pure so it is unit-tested without
|
|
16
|
+
* touching the OS (autoupdate.js precedent).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import {
|
|
21
|
+
chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { getArkaosPython } from "./python-resolver.js";
|
|
27
|
+
|
|
28
|
+
export const MENUBAR_LABEL = "io.wizardingcode.arkaos.menubar";
|
|
29
|
+
|
|
30
|
+
function defaultRepoRoot() {
|
|
31
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** XML-escape a value interpolated into plist content — `&` and `<`
|
|
35
|
+
* are legal in macOS paths and would otherwise corrupt the plist into
|
|
36
|
+
* an opaque `launchctl load` failure (QG M1). */
|
|
37
|
+
export function xmlEscape(value) {
|
|
38
|
+
return String(value)
|
|
39
|
+
.replaceAll("&", "&")
|
|
40
|
+
.replaceAll("<", "<")
|
|
41
|
+
.replaceAll(">", ">")
|
|
42
|
+
.replaceAll('"', """)
|
|
43
|
+
.replaceAll("'", "'");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pure: the opt-out marker path — written by disable(), honored by
|
|
47
|
+
* ensureDefaultEnabled(). */
|
|
48
|
+
export function optoutPath(home = homedir()) {
|
|
49
|
+
return join(home, ".arkaos", "menubar.optout");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pure: where the menu bar script lives after deployment. */
|
|
53
|
+
export function menubarScriptPath(home = homedir()) {
|
|
54
|
+
return join(home, ".arkaos", "bin", "arka-menubar.py");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Pure: the LaunchAgent plist for the menu bar app. macOS-only — throws
|
|
59
|
+
* on any other platform (callers catch and report "unsupported").
|
|
60
|
+
* KeepAlive is intentionally false: "Quit" must stick until next login;
|
|
61
|
+
* RunAtLoad brings it back at login. LimitLoadToSessionType=Aqua +
|
|
62
|
+
* ProcessType=Interactive mark it as a GUI agent (idiomatic launchd).
|
|
63
|
+
*/
|
|
64
|
+
export function unitFor(os, { home, pythonPath, scriptPath }) {
|
|
65
|
+
if (os !== "darwin") {
|
|
66
|
+
throw new Error(`menu bar launcher is macOS-only (got ${os})`);
|
|
67
|
+
}
|
|
68
|
+
const log = join(home, ".arkaos", "logs", "menubar.log");
|
|
69
|
+
// PATH: launchd defaults carry no node; ~/.arkaos/bin carries the arka
|
|
70
|
+
// wrappers. nvm/fnm installs still resolve through the app's own
|
|
71
|
+
// shutil.which guard, which logs instead of failing silently (QG M5).
|
|
72
|
+
const pathValue = `/opt/homebrew/bin:/usr/local/bin:${join(home, ".arkaos", "bin")}:/usr/bin:/bin:/usr/sbin:/sbin`;
|
|
73
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
74
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
75
|
+
<plist version="1.0">
|
|
76
|
+
<dict>
|
|
77
|
+
<key>Label</key>
|
|
78
|
+
<string>${MENUBAR_LABEL}</string>
|
|
79
|
+
<key>ProgramArguments</key>
|
|
80
|
+
<array>
|
|
81
|
+
<string>${xmlEscape(pythonPath)}</string>
|
|
82
|
+
<string>${xmlEscape(scriptPath)}</string>
|
|
83
|
+
</array>
|
|
84
|
+
<key>EnvironmentVariables</key>
|
|
85
|
+
<dict>
|
|
86
|
+
<key>PATH</key>
|
|
87
|
+
<string>${xmlEscape(pathValue)}</string>
|
|
88
|
+
</dict>
|
|
89
|
+
<key>RunAtLoad</key>
|
|
90
|
+
<true/>
|
|
91
|
+
<key>KeepAlive</key>
|
|
92
|
+
<false/>
|
|
93
|
+
<key>LimitLoadToSessionType</key>
|
|
94
|
+
<string>Aqua</string>
|
|
95
|
+
<key>ProcessType</key>
|
|
96
|
+
<string>Interactive</string>
|
|
97
|
+
<key>StandardOutPath</key>
|
|
98
|
+
<string>${xmlEscape(log)}</string>
|
|
99
|
+
<key>StandardErrorPath</key>
|
|
100
|
+
<string>${xmlEscape(log)}</string>
|
|
101
|
+
</dict>
|
|
102
|
+
</plist>
|
|
103
|
+
`;
|
|
104
|
+
return {
|
|
105
|
+
kind: "launchd",
|
|
106
|
+
files: [
|
|
107
|
+
{
|
|
108
|
+
path: join(home, "Library", "LaunchAgents", `${MENUBAR_LABEL}.plist`),
|
|
109
|
+
content,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Copy bin/arka-menubar.py into ~/.arkaos/bin/ (mirrors the
|
|
117
|
+
* scheduler-daemon.py deployment). Returns true when deployed.
|
|
118
|
+
*/
|
|
119
|
+
export function deployMenubarScript({ repoRoot = defaultRepoRoot(), home = homedir() } = {}) {
|
|
120
|
+
const src = join(repoRoot, "bin", "arka-menubar.py");
|
|
121
|
+
if (!existsSync(src)) return false;
|
|
122
|
+
const dest = menubarScriptPath(home);
|
|
123
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
124
|
+
copyFileSync(src, dest);
|
|
125
|
+
try { chmodSync(dest, 0o755); } catch { /* NTFS/ACL no-op */ }
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Shell-free exec (QG M1): argv array via execFileSync — a home path
|
|
130
|
+
// containing quotes/backticks/$() can never escape into a shell.
|
|
131
|
+
function _silent(file, args) {
|
|
132
|
+
try {
|
|
133
|
+
execFileSync(file, args, { stdio: "pipe" });
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function enable({ repoRoot = defaultRepoRoot(), home = homedir() } = {}) {
|
|
141
|
+
if (process.platform !== "darwin") {
|
|
142
|
+
return { ok: false, path: "", message: "Menu bar launcher is macOS-only." };
|
|
143
|
+
}
|
|
144
|
+
if (!deployMenubarScript({ repoRoot, home })) {
|
|
145
|
+
return { ok: false, path: "", message: "bin/arka-menubar.py not found in the package." };
|
|
146
|
+
}
|
|
147
|
+
const pythonPath = getArkaosPython() || "/usr/bin/python3";
|
|
148
|
+
const unit = unitFor(process.platform, {
|
|
149
|
+
home,
|
|
150
|
+
pythonPath,
|
|
151
|
+
scriptPath: menubarScriptPath(home),
|
|
152
|
+
});
|
|
153
|
+
mkdirSync(join(home, ".arkaos", "logs"), { recursive: true });
|
|
154
|
+
for (const f of unit.files) {
|
|
155
|
+
mkdirSync(dirname(f.path), { recursive: true });
|
|
156
|
+
writeFileSync(f.path, f.content, "utf8");
|
|
157
|
+
}
|
|
158
|
+
if (existsSync(optoutPath(home))) rmSync(optoutPath(home));
|
|
159
|
+
const mainPath = unit.files[0].path;
|
|
160
|
+
_silent("launchctl", ["unload", mainPath]);
|
|
161
|
+
if (!_silent("launchctl", ["load", "-w", mainPath])) {
|
|
162
|
+
return { ok: false, path: mainPath,
|
|
163
|
+
message: "Plist written but launchctl load failed — it will still start at next login." };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, path: mainPath,
|
|
166
|
+
message: "Menu bar launcher enabled (look for ▲ in the macOS menu bar)." };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function disable({ home = homedir() } = {}) {
|
|
170
|
+
if (process.platform !== "darwin") {
|
|
171
|
+
return { ok: false, path: "", message: "Menu bar launcher is macOS-only." };
|
|
172
|
+
}
|
|
173
|
+
const plist = join(home, "Library", "LaunchAgents", `${MENUBAR_LABEL}.plist`);
|
|
174
|
+
_silent("launchctl", ["unload", plist]);
|
|
175
|
+
if (existsSync(plist)) rmSync(plist);
|
|
176
|
+
// Persisted opt-out: future `npx arkaos update` runs must not re-enable.
|
|
177
|
+
mkdirSync(join(home, ".arkaos"), { recursive: true });
|
|
178
|
+
writeFileSync(optoutPath(home), new Date().toISOString(), "utf8");
|
|
179
|
+
return { ok: true, path: plist, message: "Menu bar launcher disabled (opt-out persisted)." };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function status({ home = homedir(), platform = process.platform } = {}) {
|
|
183
|
+
if (platform !== "darwin") {
|
|
184
|
+
return { installed: false, supported: false, optout: false,
|
|
185
|
+
message: "menu bar launcher is macOS-only" };
|
|
186
|
+
}
|
|
187
|
+
const plist = join(home, "Library", "LaunchAgents", `${MENUBAR_LABEL}.plist`);
|
|
188
|
+
return {
|
|
189
|
+
installed: existsSync(plist) && existsSync(menubarScriptPath(home)),
|
|
190
|
+
supported: true,
|
|
191
|
+
optout: existsSync(optoutPath(home)),
|
|
192
|
+
kind: "launchd",
|
|
193
|
+
path: plist,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Doctor probe (QG M2): file presence alone cannot detect the primary
|
|
199
|
+
* failure mode (agent files present, process dead at import). Composes
|
|
200
|
+
* status() with a live `launchctl list <label>` query (exit 0 iff the
|
|
201
|
+
* job is loaded in this session). `exec` and `platform` are injectable
|
|
202
|
+
* so every branch is testable without touching launchd.
|
|
203
|
+
*/
|
|
204
|
+
export function menubarHealthy({
|
|
205
|
+
home = homedir(),
|
|
206
|
+
platform = process.platform,
|
|
207
|
+
exec = (file, args) => _silent(file, args),
|
|
208
|
+
} = {}) {
|
|
209
|
+
if (platform !== "darwin") return true; // not applicable
|
|
210
|
+
// platform flows down — status() reading the REAL process.platform
|
|
211
|
+
// here broke the injectable branches on the Linux CI runner (the
|
|
212
|
+
// opt-out branch returned false off-macOS).
|
|
213
|
+
const s = status({ home, platform });
|
|
214
|
+
// A persisted opt-out is a healthy, chosen state — not a warning.
|
|
215
|
+
if (s.optout) return true;
|
|
216
|
+
if (!s.installed) return false;
|
|
217
|
+
return exec("launchctl", ["list", MENUBAR_LABEL]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Default-on wiring for install/update flows: enable unless the user
|
|
221
|
+
* opted out or the platform is unsupported. Never throws. */
|
|
222
|
+
export function ensureDefaultEnabled({ repoRoot = defaultRepoRoot(), home = homedir() } = {}) {
|
|
223
|
+
const s = status({ home });
|
|
224
|
+
if (!s.supported) return { action: "unsupported" };
|
|
225
|
+
if (s.optout) return { action: "optout" };
|
|
226
|
+
if (s.installed) {
|
|
227
|
+
// Refresh script + plist content in place (paths may change between
|
|
228
|
+
// versions). Deliberately no unload/load here: the refreshed unit
|
|
229
|
+
// only takes effect at next login — a reload storm on every update
|
|
230
|
+
// would flicker the running app (QG minor, documented honestly).
|
|
231
|
+
try {
|
|
232
|
+
deployMenubarScript({ repoRoot, home });
|
|
233
|
+
const pythonPath = getArkaosPython() || "/usr/bin/python3";
|
|
234
|
+
const unit = unitFor(process.platform, {
|
|
235
|
+
home, pythonPath, scriptPath: menubarScriptPath(home),
|
|
236
|
+
});
|
|
237
|
+
for (const f of unit.files) writeFileSync(f.path, f.content, "utf8");
|
|
238
|
+
} catch { /* refresh is best-effort */ }
|
|
239
|
+
return { action: "already-enabled" };
|
|
240
|
+
}
|
|
241
|
+
const r = enable({ repoRoot, home });
|
|
242
|
+
return { action: r.ok ? "enabled" : "partial", message: r.message };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function menubar(args = []) {
|
|
246
|
+
const action = (args[0] || "status").toLowerCase();
|
|
247
|
+
if (action === "enable") {
|
|
248
|
+
const r = enable();
|
|
249
|
+
console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}${r.path ? `\n ${r.path}` : ""}`);
|
|
250
|
+
} else if (action === "disable") {
|
|
251
|
+
const r = disable();
|
|
252
|
+
console.log(` ${r.ok ? "✓" : "⚠"} ${r.message}`);
|
|
253
|
+
} else if (action === "status") {
|
|
254
|
+
const s = status();
|
|
255
|
+
if (!s.supported) console.log(` Menu bar launcher not supported here: ${s.message}`);
|
|
256
|
+
else if (s.optout) console.log(` Menu bar: DISABLED by user opt-out\n (re-enable: npx arkaos menubar enable)`);
|
|
257
|
+
else console.log(` Menu bar: ${s.installed ? "ENABLED" : "disabled"} (${s.kind})\n ${s.path}`);
|
|
258
|
+
} else {
|
|
259
|
+
console.error(` Unknown menubar action: ${action}. Use enable | disable | status.`);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product stats — derived at runtime, never hand-typed (Foundation PR-3
|
|
3
|
+
* QG minor; docs-as-code rule: counts come from the shipped artifacts).
|
|
4
|
+
*
|
|
5
|
+
* Sources (all ship in the npm tarball — see package.json "files"):
|
|
6
|
+
* agents knowledge/agents-registry-v2.json → agents[].length
|
|
7
|
+
* departments departments/ → directory count
|
|
8
|
+
* skills knowledge/skills-manifest.json → skills map +
|
|
9
|
+
* structural surface (main + hubs + meta) — the full
|
|
10
|
+
* deployable skill surface installer/skill-deploy.js ships
|
|
11
|
+
*
|
|
12
|
+
* Contract: any missing/corrupt source yields null for THAT field only.
|
|
13
|
+
* Callers must omit the corresponding output line — an invented number
|
|
14
|
+
* is worse than no number.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
export function readProductStats(repoRoot) {
|
|
21
|
+
return {
|
|
22
|
+
agents: countAgents(repoRoot),
|
|
23
|
+
departments: countDepartments(repoRoot),
|
|
24
|
+
skills: countSkills(repoRoot),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render the summary lines for the install report. Null fields are
|
|
30
|
+
* omitted; when the department count is unavailable the agents line
|
|
31
|
+
* degrades to the bare count instead of inventing a denominator.
|
|
32
|
+
*/
|
|
33
|
+
export function productStatsLines(stats) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
if (stats.agents !== null) {
|
|
36
|
+
lines.push(
|
|
37
|
+
stats.departments !== null
|
|
38
|
+
? `Agents: ${stats.agents} across ${stats.departments} departments`
|
|
39
|
+
: `Agents: ${stats.agents}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (stats.skills !== null) {
|
|
43
|
+
lines.push(`Skills: ${stats.skills} backed by enterprise frameworks`);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function countAgents(repoRoot) {
|
|
49
|
+
try {
|
|
50
|
+
const registry = JSON.parse(
|
|
51
|
+
readFileSync(join(repoRoot, "knowledge", "agents-registry-v2.json"), "utf-8"),
|
|
52
|
+
);
|
|
53
|
+
if (Array.isArray(registry.agents)) return registry.agents.length;
|
|
54
|
+
return null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function countDepartments(repoRoot) {
|
|
61
|
+
try {
|
|
62
|
+
const base = join(repoRoot, "departments");
|
|
63
|
+
if (!existsSync(base)) return null;
|
|
64
|
+
const count = readdirSync(base, { withFileTypes: true }).filter((e) =>
|
|
65
|
+
e.isDirectory(),
|
|
66
|
+
).length;
|
|
67
|
+
return count > 0 ? count : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function countSkills(repoRoot) {
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(
|
|
76
|
+
readFileSync(join(repoRoot, "knowledge", "skills-manifest.json"), "utf-8"),
|
|
77
|
+
);
|
|
78
|
+
const skills = manifest.skills;
|
|
79
|
+
if (!skills || typeof skills !== "object" || Array.isArray(skills)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
let count = Object.keys(skills).length;
|
|
83
|
+
const structural = manifest.structural;
|
|
84
|
+
if (structural && typeof structural === "object") {
|
|
85
|
+
if (structural.main) count += 1;
|
|
86
|
+
if (Array.isArray(structural.hubs)) count += structural.hubs.length;
|
|
87
|
+
if (Array.isArray(structural.meta)) count += structural.meta.length;
|
|
88
|
+
}
|
|
89
|
+
return count;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install profiles — pure, testable helpers (Foundation PR-3).
|
|
3
|
+
*
|
|
4
|
+
* A profile names WHAT gets provisioned on a machine:
|
|
5
|
+
*
|
|
6
|
+
* essential venv, hooks, skills, dashboard, MCPs
|
|
7
|
+
* complete essential + litellm[proxy] + ffmpeg
|
|
8
|
+
* local-ai complete + Ollama + local execution model
|
|
9
|
+
*
|
|
10
|
+
* In PR-3 the chosen profile is only PERSISTED (profile.json,
|
|
11
|
+
* `installProfile` key) — provisioning of the extra tiers lands in
|
|
12
|
+
* PR-4. Keeping the record builder here (instead of inline in
|
|
13
|
+
* installer/index.js) makes the persistence contract unit-testable
|
|
14
|
+
* without touching the filesystem.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const INSTALL_PROFILES = ["essential", "complete", "local-ai"];
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_PROFILE = "essential";
|
|
20
|
+
|
|
21
|
+
/** Human-readable hint per profile — single source for both wizards. */
|
|
22
|
+
export const PROFILE_HINTS = {
|
|
23
|
+
essential: "venv, hooks, skills, dashboard, MCPs",
|
|
24
|
+
complete: "essential + litellm[proxy] + ffmpeg",
|
|
25
|
+
"local-ai": "complete + Ollama + local execution model",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a user-supplied profile value (CLI flag, profile.json field).
|
|
30
|
+
* Returns the canonical profile string, or null when invalid/empty so
|
|
31
|
+
* callers can distinguish "not provided" from "provided and valid".
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeProfileFlag(value) {
|
|
34
|
+
if (typeof value !== "string") return null;
|
|
35
|
+
const v = value.trim().toLowerCase();
|
|
36
|
+
return INSTALL_PROFILES.includes(v) ? v : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether `current` covers `required` on the linear profile ladder
|
|
41
|
+
* (essential ⊂ complete ⊂ local-ai — the same chain the manifest's
|
|
42
|
+
* extends encodes). Invalid values degrade to the default profile.
|
|
43
|
+
*/
|
|
44
|
+
export function profileIncludes(current, required) {
|
|
45
|
+
const c = INSTALL_PROFILES.indexOf(normalizeProfileFlag(current) || DEFAULT_PROFILE);
|
|
46
|
+
const r = INSTALL_PROFILES.indexOf(normalizeProfileFlag(required) || DEFAULT_PROFILE);
|
|
47
|
+
return c >= r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the profile.json record persisted by installer/index.js.
|
|
52
|
+
*
|
|
53
|
+
* Mirrors the object previously built inline in install() step 11, plus
|
|
54
|
+
* the `installProfile` key. `created` is preserved from the previous
|
|
55
|
+
* record when present (upgrade), `updated` is always now.
|
|
56
|
+
*/
|
|
57
|
+
export function buildProfileRecord(userConfig = {}, previousProfile = null) {
|
|
58
|
+
const prev =
|
|
59
|
+
previousProfile && typeof previousProfile === "object" ? previousProfile : {};
|
|
60
|
+
return {
|
|
61
|
+
version: "2",
|
|
62
|
+
language: userConfig.language,
|
|
63
|
+
market: userConfig.market,
|
|
64
|
+
role: userConfig.role,
|
|
65
|
+
company: userConfig.company,
|
|
66
|
+
projectsDir: userConfig.projectsDir,
|
|
67
|
+
vaultPath: userConfig.vaultPath,
|
|
68
|
+
installProfile:
|
|
69
|
+
normalizeProfileFlag(userConfig.installProfile) ||
|
|
70
|
+
normalizeProfileFlag(prev.installProfile) ||
|
|
71
|
+
DEFAULT_PROFILE,
|
|
72
|
+
created: prev.created || new Date().toISOString(),
|
|
73
|
+
updated: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|