@0dai-dev/cli 4.2.0 → 4.3.5
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/README.md +98 -10
- package/bin/0dai.js +298 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +344 -98
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +39 -1
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +504 -28
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +27 -3
- package/lib/commands/paste.js +114 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +69 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +104 -7
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +30 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- package/lib/commands/upgrade.js +58 -0
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/onboarding.js +9 -3
- package/lib/shared.js +29 -14
- package/lib/utils/activation_telemetry.js +156 -0
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/plan.js +47 -2
- package/lib/utils/run_cost.js +91 -0
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +8 -4
- package/lib/tui/index.mjs +0 -34610
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const shared = require("../shared");
|
|
4
|
+
const { log, D, R, fs, path, spawnSync } = shared;
|
|
5
|
+
|
|
6
|
+
const BRANCH_PROTECTION_RULES = {
|
|
7
|
+
required_status_checks: {
|
|
8
|
+
strict: true,
|
|
9
|
+
contexts: [],
|
|
10
|
+
checks: [
|
|
11
|
+
{ context: "dirty-tree", app_id: -1 },
|
|
12
|
+
{ context: "lint", app_id: -1 },
|
|
13
|
+
{ context: "test", app_id: -1 },
|
|
14
|
+
{ context: "file-size", app_id: -1 },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
enforce_admins: false,
|
|
18
|
+
required_pull_request_reviews: {
|
|
19
|
+
dismiss_stale_reviews: true,
|
|
20
|
+
require_code_owner_reviews: false,
|
|
21
|
+
required_approving_review_count: 1,
|
|
22
|
+
require_last_push_approval: true,
|
|
23
|
+
},
|
|
24
|
+
restrictions: {
|
|
25
|
+
users: [],
|
|
26
|
+
teams: [],
|
|
27
|
+
apps: [],
|
|
28
|
+
},
|
|
29
|
+
required_linear_history: true,
|
|
30
|
+
allow_force_pushes: false,
|
|
31
|
+
allow_deletions: false,
|
|
32
|
+
block_creations: false,
|
|
33
|
+
required_conversation_resolution: true,
|
|
34
|
+
lock_branch: false,
|
|
35
|
+
allow_fork_syncing: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const POLICY_FILES = [
|
|
39
|
+
{
|
|
40
|
+
rel: ".github/.0dai/branch-protection-rules.json",
|
|
41
|
+
content: () => JSON.stringify(BRANCH_PROTECTION_RULES, null, 2) + "\n",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
rel: ".github/PULL_REQUEST_TEMPLATE.md",
|
|
45
|
+
content: () => [
|
|
46
|
+
"## What Changed",
|
|
47
|
+
"",
|
|
48
|
+
"- ",
|
|
49
|
+
"",
|
|
50
|
+
"## Linked Work",
|
|
51
|
+
"",
|
|
52
|
+
"- Closes #",
|
|
53
|
+
"",
|
|
54
|
+
"## Validation",
|
|
55
|
+
"",
|
|
56
|
+
"- [ ] Lint passed",
|
|
57
|
+
"- [ ] Tests passed",
|
|
58
|
+
"- [ ] Dirty-tree check passed",
|
|
59
|
+
"",
|
|
60
|
+
"## Release Impact",
|
|
61
|
+
"",
|
|
62
|
+
"- [ ] No release note needed",
|
|
63
|
+
"- [ ] Update `CHANGELOG.md`",
|
|
64
|
+
"- [ ] Add or update `release-notes/`",
|
|
65
|
+
"",
|
|
66
|
+
"## Rollback",
|
|
67
|
+
"",
|
|
68
|
+
"- Reversal: `git revert <merge-sha>`",
|
|
69
|
+
"- Data migration: none",
|
|
70
|
+
"",
|
|
71
|
+
].join("\n"),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
rel: ".github/workflows/ci.yml",
|
|
75
|
+
content: () => [
|
|
76
|
+
"name: CI",
|
|
77
|
+
"",
|
|
78
|
+
"on:",
|
|
79
|
+
" pull_request:",
|
|
80
|
+
" branches: [main]",
|
|
81
|
+
" push:",
|
|
82
|
+
" branches: [main]",
|
|
83
|
+
"",
|
|
84
|
+
"permissions:",
|
|
85
|
+
" contents: read",
|
|
86
|
+
"",
|
|
87
|
+
"jobs:",
|
|
88
|
+
" dirty-tree:",
|
|
89
|
+
" runs-on: ubuntu-latest",
|
|
90
|
+
" steps:",
|
|
91
|
+
" - uses: actions/checkout@v4",
|
|
92
|
+
" - name: Generated file guard",
|
|
93
|
+
" run: git diff --exit-code",
|
|
94
|
+
"",
|
|
95
|
+
" lint:",
|
|
96
|
+
" runs-on: ubuntu-latest",
|
|
97
|
+
" steps:",
|
|
98
|
+
" - uses: actions/checkout@v4",
|
|
99
|
+
" - uses: actions/setup-node@v4",
|
|
100
|
+
" if: hashFiles('package.json') != ''",
|
|
101
|
+
" with:",
|
|
102
|
+
" node-version: '20'",
|
|
103
|
+
" - uses: actions/setup-python@v5",
|
|
104
|
+
" if: hashFiles('pyproject.toml', 'requirements.txt', '**/*.py') != ''",
|
|
105
|
+
" with:",
|
|
106
|
+
" python-version: '3.12'",
|
|
107
|
+
" - name: Lint",
|
|
108
|
+
" run: |",
|
|
109
|
+
" if [ -f package.json ]; then npm run lint --if-present; fi",
|
|
110
|
+
" if find . -path './.git' -prune -o -name '*.py' -print -quit | grep -q .; then",
|
|
111
|
+
" python3 -m compileall -q .",
|
|
112
|
+
" fi",
|
|
113
|
+
"",
|
|
114
|
+
" test:",
|
|
115
|
+
" runs-on: ubuntu-latest",
|
|
116
|
+
" steps:",
|
|
117
|
+
" - uses: actions/checkout@v4",
|
|
118
|
+
" - uses: actions/setup-node@v4",
|
|
119
|
+
" if: hashFiles('package.json') != ''",
|
|
120
|
+
" with:",
|
|
121
|
+
" node-version: '20'",
|
|
122
|
+
" - uses: actions/setup-python@v5",
|
|
123
|
+
" if: hashFiles('pyproject.toml', 'requirements.txt', 'tests/**/*.py') != ''",
|
|
124
|
+
" with:",
|
|
125
|
+
" python-version: '3.12'",
|
|
126
|
+
" - name: Test",
|
|
127
|
+
" run: |",
|
|
128
|
+
" if [ -f package.json ]; then npm test --if-present; fi",
|
|
129
|
+
" if [ -d tests ] && find tests -name '*.py' -print -quit | grep -q .; then",
|
|
130
|
+
" python3 -m pytest",
|
|
131
|
+
" fi",
|
|
132
|
+
"",
|
|
133
|
+
" file-size:",
|
|
134
|
+
" runs-on: ubuntu-latest",
|
|
135
|
+
" steps:",
|
|
136
|
+
" - uses: actions/checkout@v4",
|
|
137
|
+
" - name: File size guard",
|
|
138
|
+
" run: |",
|
|
139
|
+
" if [ -x .githooks/check-file-size.sh ]; then",
|
|
140
|
+
" bash .githooks/check-file-size.sh",
|
|
141
|
+
" else",
|
|
142
|
+
" echo \"No .githooks/check-file-size.sh present; skipping.\"",
|
|
143
|
+
" fi",
|
|
144
|
+
"",
|
|
145
|
+
].join("\n"),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
rel: ".githooks/check-file-size.sh",
|
|
149
|
+
executable: true,
|
|
150
|
+
content: () => [
|
|
151
|
+
"#!/usr/bin/env bash",
|
|
152
|
+
"set -euo pipefail",
|
|
153
|
+
"",
|
|
154
|
+
"MAX_WARN=\"${MAX_WARN:-800}\"",
|
|
155
|
+
"MAX_FAIL=\"${MAX_FAIL:-2000}\"",
|
|
156
|
+
"WAIVER_TAG=\"# pragma: loc-waiver\"",
|
|
157
|
+
"mode=\"${1:-all}\"",
|
|
158
|
+
"",
|
|
159
|
+
"file_has_waiver() {",
|
|
160
|
+
" local file=\"$1\"",
|
|
161
|
+
" [ -f \"$file\" ] && head -n 32 \"$file\" 2>/dev/null | grep -qF \"$WAIVER_TAG\"",
|
|
162
|
+
"}",
|
|
163
|
+
"",
|
|
164
|
+
"if [ \"$mode\" = \"--staged\" ]; then",
|
|
165
|
+
" files=\"$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(py|js|ts|tsx|jsx|sh)$' || true)\"",
|
|
166
|
+
"else",
|
|
167
|
+
" files=\"$(find . -path './.git' -prune -o -path './node_modules' -prune -o -type f \\( -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.jsx' -o -name '*.sh' \\) -print 2>/dev/null || true)\"",
|
|
168
|
+
"fi",
|
|
169
|
+
"",
|
|
170
|
+
"[ -n \"$files\" ] || exit 0",
|
|
171
|
+
"",
|
|
172
|
+
"failed=0",
|
|
173
|
+
"while IFS= read -r file; do",
|
|
174
|
+
" [ -n \"$file\" ] || continue",
|
|
175
|
+
" [ -f \"$file\" ] || continue",
|
|
176
|
+
" lines=\"$(wc -l < \"$file\")\"",
|
|
177
|
+
" if [ \"$lines\" -ge \"$MAX_FAIL\" ] && ! file_has_waiver \"$file\"; then",
|
|
178
|
+
" printf 'FAIL %s: %s LOC >= %s\\n' \"$file\" \"$lines\" \"$MAX_FAIL\"",
|
|
179
|
+
" failed=$((failed + 1))",
|
|
180
|
+
" fi",
|
|
181
|
+
"done <<< \"$files\"",
|
|
182
|
+
"",
|
|
183
|
+
"[ \"$failed\" -eq 0 ] || exit 1",
|
|
184
|
+
"",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
rel: ".githooks/pre-commit",
|
|
189
|
+
executable: true,
|
|
190
|
+
content: () => [
|
|
191
|
+
"#!/usr/bin/env bash",
|
|
192
|
+
"set -euo pipefail",
|
|
193
|
+
"",
|
|
194
|
+
"repo_root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0",
|
|
195
|
+
"git diff --cached --check",
|
|
196
|
+
"if [ -x \"$repo_root/.githooks/check-file-size.sh\" ]; then",
|
|
197
|
+
" bash \"$repo_root/.githooks/check-file-size.sh\" --staged",
|
|
198
|
+
"fi",
|
|
199
|
+
"if [ -f \"$repo_root/scripts/scan_secrets.py\" ]; then",
|
|
200
|
+
" python3 \"$repo_root/scripts/scan_secrets.py\" --target \"$repo_root\"",
|
|
201
|
+
"fi",
|
|
202
|
+
"",
|
|
203
|
+
].join("\n"),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
rel: "CHANGELOG.md",
|
|
207
|
+
content: () => [
|
|
208
|
+
"# Changelog",
|
|
209
|
+
"",
|
|
210
|
+
"All notable changes to this project are documented here.",
|
|
211
|
+
"",
|
|
212
|
+
"## Unreleased",
|
|
213
|
+
"",
|
|
214
|
+
"### Added",
|
|
215
|
+
"",
|
|
216
|
+
"- Initial 0dai project policy scaffold.",
|
|
217
|
+
"",
|
|
218
|
+
].join("\n"),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
rel: "CONTRIBUTING.md",
|
|
222
|
+
content: () => [
|
|
223
|
+
"# Contributing",
|
|
224
|
+
"",
|
|
225
|
+
"## Branch Flow",
|
|
226
|
+
"",
|
|
227
|
+
"1. Branch from `main`.",
|
|
228
|
+
"2. Keep changes focused and reviewable.",
|
|
229
|
+
"3. Open a pull request back to `main`.",
|
|
230
|
+
"4. Wait for required CI checks and review before merge.",
|
|
231
|
+
"",
|
|
232
|
+
"Use descriptive branch names such as `feat/<issue>-short-topic`,",
|
|
233
|
+
"`fix/<issue>-short-topic`, or `docs/<issue>-short-topic`.",
|
|
234
|
+
"",
|
|
235
|
+
"## Commits",
|
|
236
|
+
"",
|
|
237
|
+
"- Use an imperative summary under 72 characters.",
|
|
238
|
+
"- Reference the issue or task when one exists.",
|
|
239
|
+
"- Include the why in the body for non-trivial changes.",
|
|
240
|
+
"",
|
|
241
|
+
"## Local Checks",
|
|
242
|
+
"",
|
|
243
|
+
"Run the project lint and test commands from `ai/manifest/commands.yaml` when",
|
|
244
|
+
"they are present. The generated `.githooks/` path is activated by `0dai init`",
|
|
245
|
+
"with:",
|
|
246
|
+
"",
|
|
247
|
+
"```bash",
|
|
248
|
+
"git config core.hooksPath .githooks",
|
|
249
|
+
"```",
|
|
250
|
+
"",
|
|
251
|
+
].join("\n"),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
rel: "RELEASE_NOTES_TEMPLATE.md",
|
|
255
|
+
content: () => [
|
|
256
|
+
"# Release Notes: vX.Y.Z",
|
|
257
|
+
"",
|
|
258
|
+
"## Summary",
|
|
259
|
+
"",
|
|
260
|
+
"- ",
|
|
261
|
+
"",
|
|
262
|
+
"## Changes",
|
|
263
|
+
"",
|
|
264
|
+
"- ",
|
|
265
|
+
"",
|
|
266
|
+
"## Validation",
|
|
267
|
+
"",
|
|
268
|
+
"- ",
|
|
269
|
+
"",
|
|
270
|
+
"## Rollback",
|
|
271
|
+
"",
|
|
272
|
+
"- Revert the release commit or tag.",
|
|
273
|
+
"",
|
|
274
|
+
].join("\n"),
|
|
275
|
+
},
|
|
276
|
+
{ rel: "VERSION", content: () => "0.1.0\n" },
|
|
277
|
+
{ rel: "release-notes/.gitkeep", content: () => "" },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
function parseOptions(args) {
|
|
281
|
+
const opts = { branch: "main", rules: "", dryRun: false };
|
|
282
|
+
for (let i = 0; i < args.length; i++) {
|
|
283
|
+
const arg = String(args[i] || "");
|
|
284
|
+
if (arg === "--branch" && args[i + 1]) {
|
|
285
|
+
opts.branch = String(args[++i]);
|
|
286
|
+
} else if ((arg === "--rules" || arg === "--input") && args[i + 1]) {
|
|
287
|
+
opts.rules = String(args[++i]);
|
|
288
|
+
} else if (arg === "--dry-run") {
|
|
289
|
+
opts.dryRun = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return opts;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function ensureDir(filePath) {
|
|
296
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function writeIfMissing(target, rel, content, executable) {
|
|
300
|
+
const dest = path.join(target, rel);
|
|
301
|
+
if (fs.existsSync(dest)) return false;
|
|
302
|
+
ensureDir(dest);
|
|
303
|
+
fs.writeFileSync(dest, content, "utf8");
|
|
304
|
+
if (executable) fs.chmodSync(dest, 0o755);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function scaffoldGithubFlowPolicy(target) {
|
|
309
|
+
let created = 0;
|
|
310
|
+
for (const file of POLICY_FILES) {
|
|
311
|
+
if (writeIfMissing(target, file.rel, file.content(), !!file.executable)) created += 1;
|
|
312
|
+
}
|
|
313
|
+
return created;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isGitRepo(target) {
|
|
317
|
+
const result = spawnSync("git", ["-C", target, "rev-parse", "--is-inside-work-tree"], {
|
|
318
|
+
encoding: "utf8",
|
|
319
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
320
|
+
});
|
|
321
|
+
return result.status === 0 && String(result.stdout || "").trim() === "true";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hasGithubRemote(target) {
|
|
325
|
+
const result = spawnSync("git", ["-C", target, "remote", "get-url", "origin"], {
|
|
326
|
+
encoding: "utf8",
|
|
327
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
328
|
+
});
|
|
329
|
+
if (result.status !== 0) return false;
|
|
330
|
+
return /github\.com[:/]/.test(String(result.stdout || ""));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ensureHooksPath(target) {
|
|
334
|
+
if (!isGitRepo(target)) return false;
|
|
335
|
+
const result = spawnSync("git", ["-C", target, "config", "core.hooksPath", ".githooks"], {
|
|
336
|
+
encoding: "utf8",
|
|
337
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
338
|
+
});
|
|
339
|
+
return result.status === 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getHooksPath(target) {
|
|
343
|
+
if (!isGitRepo(target)) return null;
|
|
344
|
+
const result = spawnSync("git", ["-C", target, "config", "--get", "core.hooksPath"], {
|
|
345
|
+
encoding: "utf8",
|
|
346
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
347
|
+
});
|
|
348
|
+
if (result.status !== 0) return "";
|
|
349
|
+
return String(result.stdout || "").trim();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function warnHooksPathDrift(target) {
|
|
353
|
+
const hooksPath = getHooksPath(target);
|
|
354
|
+
if (hooksPath === null || hooksPath === ".githooks") return false;
|
|
355
|
+
const current = hooksPath || "not set";
|
|
356
|
+
log(`warning: core.hooksPath is ${current}; expected .githooks for 0dai git policy`);
|
|
357
|
+
console.log(` ${D}Run: git config core.hooksPath .githooks${R}`);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function defaultRulesPath(target) {
|
|
362
|
+
return path.join(target, ".github", ".0dai", "branch-protection-rules.json");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function findInstallGitPolicyScript(target) {
|
|
366
|
+
const repoRoot = path.resolve(__dirname, "..", "..", "..", "..");
|
|
367
|
+
const candidates = [
|
|
368
|
+
path.join(target, "bootstrap", "install_git_policy.sh"),
|
|
369
|
+
path.join(process.cwd(), "bootstrap", "install_git_policy.sh"),
|
|
370
|
+
path.join(repoRoot, "bootstrap", "install_git_policy.sh"),
|
|
371
|
+
];
|
|
372
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || "";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runBootstrapInstaller(target) {
|
|
376
|
+
if (process.env.ODAI_GIT_POLICY_JS_ONLY === "1") return false;
|
|
377
|
+
const script = findInstallGitPolicyScript(target);
|
|
378
|
+
if (!script) return false;
|
|
379
|
+
const result = spawnSync("bash", [script, "--target", target], {
|
|
380
|
+
encoding: "utf8",
|
|
381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
382
|
+
});
|
|
383
|
+
if (result.status !== 0) {
|
|
384
|
+
const reason = String(result.stderr || result.stdout || "unknown error").trim().split("\n").pop();
|
|
385
|
+
log(`git policy installer skipped: ${reason}`);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function applyBranchProtection(target, options = {}) {
|
|
392
|
+
const branch = options.branch || "main";
|
|
393
|
+
const rules = options.rules || defaultRulesPath(target);
|
|
394
|
+
const failOnError = !!options.failOnError;
|
|
395
|
+
const dryRun = !!options.dryRun;
|
|
396
|
+
const inherit = !!options.inherit;
|
|
397
|
+
|
|
398
|
+
if (!fs.existsSync(rules)) {
|
|
399
|
+
const message = `branch protection rules not found: ${rules}`;
|
|
400
|
+
if (failOnError) throw new Error(message);
|
|
401
|
+
return { ok: false, skipped: true, reason: message };
|
|
402
|
+
}
|
|
403
|
+
if (!hasGithubRemote(target)) {
|
|
404
|
+
const message = "origin is not a GitHub remote";
|
|
405
|
+
if (failOnError) throw new Error(message);
|
|
406
|
+
return { ok: false, skipped: true, reason: message };
|
|
407
|
+
}
|
|
408
|
+
if (spawnSync("gh", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
409
|
+
const message = "gh CLI not found";
|
|
410
|
+
if (failOnError) throw new Error(message);
|
|
411
|
+
return { ok: false, skipped: true, reason: message };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const endpoint = `repos/:owner/:repo/branches/${branch}/protection`;
|
|
415
|
+
if (dryRun) {
|
|
416
|
+
console.log(`gh api ${endpoint} -X PUT --input ${rules}`);
|
|
417
|
+
return { ok: true, dryRun: true };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const result = spawnSync("gh", ["api", endpoint, "-X", "PUT", "--input", rules], {
|
|
421
|
+
cwd: target,
|
|
422
|
+
encoding: "utf8",
|
|
423
|
+
stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
424
|
+
});
|
|
425
|
+
if (result.status === 0) return { ok: true };
|
|
426
|
+
|
|
427
|
+
const message = String(result.stderr || result.stdout || "gh api failed").trim();
|
|
428
|
+
if (failOnError) throw new Error(message);
|
|
429
|
+
return { ok: false, skipped: false, reason: message };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function printBranchProtection(target, options = {}) {
|
|
433
|
+
const branch = options.branch || "main";
|
|
434
|
+
if (!hasGithubRemote(target)) throw new Error("origin is not a GitHub remote");
|
|
435
|
+
if (spawnSync("gh", ["--version"], { stdio: "ignore" }).status !== 0) throw new Error("gh CLI not found");
|
|
436
|
+
const result = spawnSync("gh", ["api", `repos/:owner/:repo/branches/${branch}/protection`, "-X", "GET"], {
|
|
437
|
+
cwd: target,
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
440
|
+
});
|
|
441
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || "gh api failed").trim());
|
|
442
|
+
process.stdout.write(result.stdout);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function ensureGithubFlowPolicy(target, options = {}) {
|
|
446
|
+
if (process.env.ODAI_GIT_POLICY_SKIP === "1") return { installed: false, skipped: true };
|
|
447
|
+
if (runBootstrapInstaller(target)) return { installed: true, via: "bootstrap" };
|
|
448
|
+
|
|
449
|
+
const created = scaffoldGithubFlowPolicy(target);
|
|
450
|
+
ensureHooksPath(target);
|
|
451
|
+
let protection = null;
|
|
452
|
+
if (!options.skipApply) {
|
|
453
|
+
protection = applyBranchProtection(target, {
|
|
454
|
+
branch: options.branch || "main",
|
|
455
|
+
rules: options.rules || defaultRulesPath(target),
|
|
456
|
+
failOnError: false,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return { installed: true, via: "js", created, protection };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function cmdGh(target, sub, args = []) {
|
|
463
|
+
if (sub !== "branch-protection") {
|
|
464
|
+
console.log("Usage: 0dai gh branch-protection [print|apply] [--branch main] [--rules FILE] [--dry-run]");
|
|
465
|
+
process.exitCode = 1;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const rest = args.slice(2);
|
|
470
|
+
const action = rest[0] && !String(rest[0]).startsWith("-") ? String(rest[0]) : "print";
|
|
471
|
+
const options = parseOptions(rest);
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
if (action === "apply") {
|
|
475
|
+
scaffoldGithubFlowPolicy(target);
|
|
476
|
+
ensureHooksPath(target);
|
|
477
|
+
applyBranchProtection(target, { ...options, failOnError: true, inherit: !options.dryRun });
|
|
478
|
+
if (!options.dryRun) log(`applied branch protection to ${options.branch || "main"}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (action === "print" || action === "show" || action === "current") {
|
|
482
|
+
printBranchProtection(target, options);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (action === "install") {
|
|
486
|
+
const result = ensureGithubFlowPolicy(target, { skipApply: true });
|
|
487
|
+
log(`installed git policy files${result.created ? ` (${result.created} created)` : ""}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
console.log("Usage: 0dai gh branch-protection [print|apply|install] [--branch main] [--rules FILE] [--dry-run]");
|
|
491
|
+
process.exitCode = 1;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
log(`branch-protection ${action} failed: ${err.message || err}`);
|
|
494
|
+
console.log(` ${D}Rules: ${options.rules || defaultRulesPath(target)}${R}`);
|
|
495
|
+
process.exitCode = 1;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = {
|
|
500
|
+
BRANCH_PROTECTION_RULES,
|
|
501
|
+
cmdGh,
|
|
502
|
+
ensureGithubFlowPolicy,
|
|
503
|
+
scaffoldGithubFlowPolicy,
|
|
504
|
+
applyBranchProtection,
|
|
505
|
+
warnHooksPathDrift,
|
|
506
|
+
};
|
package/lib/commands/graph.js
CHANGED
|
@@ -2,6 +2,63 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, apiCall, AUTH_FILE, buildProjectIdentity, collectMetadata, recordExperienceEvent } = shared;
|
|
4
4
|
|
|
5
|
+
function graphAuthToken(auth) {
|
|
6
|
+
return String((auth && (auth.api_key || auth.access_token || auth.token)) || "").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isPaidGraphPlan(auth) {
|
|
10
|
+
const plan = String((auth && auth.plan) || "free").toLowerCase();
|
|
11
|
+
return ["pro", "team", "enterprise"].includes(plan);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function graphPayloadForPush(localGraph, auth) {
|
|
15
|
+
const nodes = localGraph.nodes || {};
|
|
16
|
+
const localEdges = localGraph.edges || [];
|
|
17
|
+
const edges = isPaidGraphPlan(auth) ? localEdges : [];
|
|
18
|
+
return {
|
|
19
|
+
nodes,
|
|
20
|
+
edges,
|
|
21
|
+
localEdgeCount: localEdges.length,
|
|
22
|
+
edgesLocalOnly: localEdges.length - edges.length,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readStructuralMemorySummary(target) {
|
|
27
|
+
const script = shared.findRepoScript(target, "structural_memory.py");
|
|
28
|
+
if (!script) return null;
|
|
29
|
+
const result = shared.spawnSync("python3", [script, "--target", target, "--json"], {
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
timeout: 10000,
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0 || !result.stdout) return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(result.stdout);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatStructuralMemorySummary(summary) {
|
|
42
|
+
if (!summary || typeof summary !== "object") return [];
|
|
43
|
+
const surfaces = summary.surfaces || {};
|
|
44
|
+
const roles = summary.roles || {};
|
|
45
|
+
const parts = [
|
|
46
|
+
`records=${summary.total_records || 0}`,
|
|
47
|
+
`memory=${surfaces.memory || 0}`,
|
|
48
|
+
`graph=${surfaces.graph || 0}`,
|
|
49
|
+
`experience=${surfaces.experience || 0}`,
|
|
50
|
+
];
|
|
51
|
+
const roleParts = Object.entries(roles)
|
|
52
|
+
.filter(([, count]) => Number(count) > 0)
|
|
53
|
+
.slice(0, 6)
|
|
54
|
+
.map(([role, count]) => `${role}:${count}`);
|
|
55
|
+
const lines = [`Structural memory: ${parts.join(", ")}`];
|
|
56
|
+
if (roleParts.length) lines.push(` roles: ${roleParts.join(", ")}`);
|
|
57
|
+
const driftCount = Number(summary.drift_warning_count || 0);
|
|
58
|
+
if (driftCount > 0) lines.push(` drift warnings: ${driftCount}`);
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
5
62
|
async function cmdGraph(target, sub, args) {
|
|
6
63
|
const graphFile = path.join(target, "ai", "manifest", "project_graph.json");
|
|
7
64
|
|
|
@@ -9,7 +66,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
9
66
|
// Check auth
|
|
10
67
|
let auth;
|
|
11
68
|
try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
|
|
12
|
-
if (!auth
|
|
69
|
+
if (!graphAuthToken(auth)) {
|
|
13
70
|
log("Graph push requires an account. Run: 0dai auth login");
|
|
14
71
|
return;
|
|
15
72
|
}
|
|
@@ -25,11 +82,11 @@ async function cmdGraph(target, sub, args) {
|
|
|
25
82
|
return;
|
|
26
83
|
}
|
|
27
84
|
|
|
28
|
-
const nodes = localGraph
|
|
29
|
-
const edges = localGraph.edges || [];
|
|
85
|
+
const { nodes, edges, localEdgeCount, edgesLocalOnly } = graphPayloadForPush(localGraph, auth);
|
|
30
86
|
const identity = buildProjectIdentity(target, collectMetadata(target));
|
|
31
87
|
|
|
32
|
-
|
|
88
|
+
const edgeSuffix = edgesLocalOnly > 0 ? `, ${edgesLocalOnly} local-only edges held back` : "";
|
|
89
|
+
log(`Pushing graph (${Object.keys(nodes).length} nodes, ${edges.length} edges${edgeSuffix})...`);
|
|
33
90
|
const result = await apiCall("/v1/graph/sync", {
|
|
34
91
|
project_id: identity.project_id,
|
|
35
92
|
nodes,
|
|
@@ -58,7 +115,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
58
115
|
model: "0dai-cli",
|
|
59
116
|
effort: "medium",
|
|
60
117
|
task: { goal: "push graph to server", task_type: "feat", result: "success", elapsed_seconds: 0, cost_usd: 0 },
|
|
61
|
-
context: { stack: "unknown", files_touched: 1, tests_passed: true, graph_nodes_used: Object.keys(nodes).length, graph_edges_used: edges.length },
|
|
118
|
+
context: { stack: "unknown", files_touched: 1, tests_passed: true, graph_nodes_used: Object.keys(nodes).length, graph_edges_used: edges.length, graph_edges_local_only: localEdgeCount - edges.length },
|
|
62
119
|
});
|
|
63
120
|
return;
|
|
64
121
|
}
|
|
@@ -66,7 +123,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
66
123
|
if (sub === "pull") {
|
|
67
124
|
let auth;
|
|
68
125
|
try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
|
|
69
|
-
if (!auth
|
|
126
|
+
if (!graphAuthToken(auth)) {
|
|
70
127
|
log("Graph pull requires an account. Run: 0dai auth login");
|
|
71
128
|
return;
|
|
72
129
|
}
|
|
@@ -148,11 +205,15 @@ async function cmdGraph(target, sub, args) {
|
|
|
148
205
|
}
|
|
149
206
|
|
|
150
207
|
log(`Local graph: ${localNodes} nodes, ${localEdges} edges`);
|
|
208
|
+
const structuralSummary = readStructuralMemorySummary(target);
|
|
209
|
+
for (const line of formatStructuralMemorySummary(structuralSummary)) {
|
|
210
|
+
log(line);
|
|
211
|
+
}
|
|
151
212
|
|
|
152
213
|
// Check plan for edge sync status
|
|
153
214
|
let auth;
|
|
154
215
|
try { auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8")); } catch {}
|
|
155
|
-
if (auth
|
|
216
|
+
if (graphAuthToken(auth)) {
|
|
156
217
|
const plan = (auth.plan || "free").toLowerCase();
|
|
157
218
|
const edgeStatus = plan === "free" ? `${D}nodes only (Pro for edges)${R}` : `${T}full sync${R}`;
|
|
158
219
|
log(`Server sync: ${edgeStatus}`);
|
|
@@ -174,7 +235,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
174
235
|
const nodeIdx = args.indexOf("--node");
|
|
175
236
|
const nodeId = nodeIdx >= 0 ? args[nodeIdx + 1] : null;
|
|
176
237
|
|
|
177
|
-
if (["pro", "team", "enterprise"].includes(plan) && auth
|
|
238
|
+
if (["pro", "team", "enterprise"].includes(plan) && graphAuthToken(auth)) {
|
|
178
239
|
const identity = buildProjectIdentity(target, collectMetadata(target));
|
|
179
240
|
const params = new URLSearchParams({
|
|
180
241
|
project_id: identity.project_id,
|
|
@@ -236,7 +297,7 @@ async function cmdGraph(target, sub, args) {
|
|
|
236
297
|
log(`${D}Outcomes require Pro plan. Upgrade: 0dai upgrade${R}`);
|
|
237
298
|
return;
|
|
238
299
|
}
|
|
239
|
-
if (!auth
|
|
300
|
+
if (!graphAuthToken(auth)) {
|
|
240
301
|
log("Graph outcomes requires an account. Run: 0dai auth login");
|
|
241
302
|
return;
|
|
242
303
|
}
|
|
@@ -267,4 +328,11 @@ async function cmdGraph(target, sub, args) {
|
|
|
267
328
|
console.log(" outcomes Show outcome analytics (Pro only)");
|
|
268
329
|
}
|
|
269
330
|
|
|
270
|
-
module.exports = {
|
|
331
|
+
module.exports = {
|
|
332
|
+
cmdGraph,
|
|
333
|
+
graphAuthToken,
|
|
334
|
+
graphPayloadForPush,
|
|
335
|
+
isPaidGraphPlan,
|
|
336
|
+
readStructuralMemorySummary,
|
|
337
|
+
formatStructuralMemorySummary,
|
|
338
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const shared = require("../shared");
|
|
3
|
+
const { log, spawnSync, findRepoScript } = shared;
|
|
4
|
+
|
|
5
|
+
function cmdHeatmap(target, args) {
|
|
6
|
+
const script = findRepoScript(target, "heatmap.py");
|
|
7
|
+
if (!script) {
|
|
8
|
+
log("heatmap script unavailable in this environment");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const forwarded = [script, "--target", target, ...(args || [])];
|
|
13
|
+
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
14
|
+
if (typeof result.status === "number" && result.status !== 0) process.exit(result.status);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { cmdHeatmap };
|