@0dai-dev/cli 4.3.5 → 4.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/0dai.js +87 -10
- package/lib/commands/auth.js +53 -0
- package/lib/commands/detect.js +10 -4
- package/lib/commands/doctor.js +42 -17
- package/lib/commands/export.js +73 -0
- package/lib/commands/init.js +14 -4
- package/lib/commands/mcp.js +33 -3
- package/lib/commands/run.js +4 -1
- package/lib/commands/status.js +6 -1
- package/lib/commands/trust.js +286 -0
- package/lib/commands/vault.js +3 -1
- package/lib/shared.js +2 -2
- package/lib/utils/activation_telemetry.js +233 -11
- package/lib/utils/export-bundler.js +285 -0
- package/lib/utils/identity.js +14 -1
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 0dai.dev
|
|
3
|
+
//
|
|
4
|
+
// F14 G4 Phase 2 — tarball builder for `0dai export --all`.
|
|
5
|
+
//
|
|
6
|
+
// Honors the layout pinned in docs/governance/data-export-contract.md
|
|
7
|
+
// (F14 G4 Phase 1 / PR #3569). Phase 2 ships personas +
|
|
8
|
+
// path-protect.yaml as real data, usage-ledger.jsonl when present,
|
|
9
|
+
// and 6 placeholders for the rest.
|
|
10
|
+
//
|
|
11
|
+
// Uses `tar` shell command (always present on Linux/macOS) rather
|
|
12
|
+
// than the `tar` npm module to avoid a new runtime dep.
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const crypto = require("crypto");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const { spawnSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
const PHASE = "F14 G4 Phase 2";
|
|
23
|
+
const SCHEMA_VERSION = "1";
|
|
24
|
+
|
|
25
|
+
const PLACEHOLDER_SURFACES = [
|
|
26
|
+
// (surface name, tarball relative path)
|
|
27
|
+
["knowledge-graph", "knowledge-graph/entries.jsonl"],
|
|
28
|
+
["audit-log", "audit-log/events.jsonl"],
|
|
29
|
+
["team-config", "team/config.yaml"],
|
|
30
|
+
["repo-links", "repo-links.json"],
|
|
31
|
+
["webhooks", "webhooks.yaml"],
|
|
32
|
+
["feature-flags", "feature-flags.yaml"],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function sha256OfFile(filePath) {
|
|
36
|
+
const hash = crypto.createHash("sha256");
|
|
37
|
+
const fd = fs.openSync(filePath, "r");
|
|
38
|
+
const buf = Buffer.allocUnsafe(1024 * 1024);
|
|
39
|
+
try {
|
|
40
|
+
let n = 0;
|
|
41
|
+
do {
|
|
42
|
+
n = fs.readSync(fd, buf, 0, buf.length, null);
|
|
43
|
+
if (n > 0) hash.update(buf.subarray(0, n));
|
|
44
|
+
} while (n > 0);
|
|
45
|
+
} finally {
|
|
46
|
+
fs.closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
return hash.digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sha256OfBytes(bytes) {
|
|
52
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function listPersonas(sourceRoot) {
|
|
56
|
+
const personasDir = path.join(sourceRoot, "ai", "personas");
|
|
57
|
+
if (!fs.existsSync(personasDir)) return [];
|
|
58
|
+
return fs.readdirSync(personasDir)
|
|
59
|
+
.filter((name) => name.endsWith(".yaml") || name.endsWith(".yml"))
|
|
60
|
+
.map((name) => ({
|
|
61
|
+
name,
|
|
62
|
+
sourcePath: path.join(personasDir, name),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pathProtectPath(sourceRoot) {
|
|
67
|
+
const p = path.join(sourceRoot, "ai", "policy", "path-protect.yaml");
|
|
68
|
+
return fs.existsSync(p) ? p : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function usageLedgerPath(sourceRoot) {
|
|
72
|
+
const p = path.join(sourceRoot, "ai", "telemetry", "usage-ledger.jsonl");
|
|
73
|
+
return fs.existsSync(p) ? p : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readme(tenantId) {
|
|
77
|
+
return `# 0dai export bundle
|
|
78
|
+
|
|
79
|
+
Generated by \`0dai export --all\` (F14 G4 Phase 2).
|
|
80
|
+
|
|
81
|
+
Verify per docs/governance/data-export-contract.md.
|
|
82
|
+
|
|
83
|
+
Tenant: ${tenantId}
|
|
84
|
+
Schema version: ${SCHEMA_VERSION}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function placeholderPayload(surface) {
|
|
89
|
+
return JSON.stringify({
|
|
90
|
+
_status: "not-implemented",
|
|
91
|
+
_surface: surface,
|
|
92
|
+
_since: PHASE,
|
|
93
|
+
_note: "Phase 3 will populate this surface; today it ships as a placeholder so re-import tooling can validate the layout.",
|
|
94
|
+
}, null, 2) + "\n";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function buildExportTarball({ sourceRoot, outputPath }) {
|
|
98
|
+
if (!sourceRoot || !fs.existsSync(sourceRoot)) {
|
|
99
|
+
throw new Error(`source root not found: ${sourceRoot}`);
|
|
100
|
+
}
|
|
101
|
+
const outDir = path.dirname(path.resolve(outputPath));
|
|
102
|
+
if (!fs.existsSync(outDir)) {
|
|
103
|
+
throw new Error(`output dir does not exist: ${outDir}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), "0dai-export-"));
|
|
107
|
+
try {
|
|
108
|
+
// README.md
|
|
109
|
+
const tenantId = path.basename(sourceRoot);
|
|
110
|
+
const readmeBytes = Buffer.from(readme(tenantId), "utf8");
|
|
111
|
+
fs.writeFileSync(path.join(stagingRoot, "README.md"), readmeBytes);
|
|
112
|
+
|
|
113
|
+
// tenant.json (stub — Phase 3 populates from API)
|
|
114
|
+
const tenantBytes = Buffer.from(JSON.stringify({
|
|
115
|
+
_status: "stub",
|
|
116
|
+
_since: PHASE,
|
|
117
|
+
tenant_id: tenantId,
|
|
118
|
+
exported_at: new Date().toISOString(),
|
|
119
|
+
}, null, 2) + "\n", "utf8");
|
|
120
|
+
fs.writeFileSync(path.join(stagingRoot, "tenant.json"), tenantBytes);
|
|
121
|
+
|
|
122
|
+
// personas/<name>.yaml (real data)
|
|
123
|
+
const personasOutDir = path.join(stagingRoot, "personas");
|
|
124
|
+
fs.mkdirSync(personasOutDir, { recursive: true });
|
|
125
|
+
const personas = listPersonas(sourceRoot);
|
|
126
|
+
const personaManifest = [];
|
|
127
|
+
for (const p of personas) {
|
|
128
|
+
const dest = path.join(personasOutDir, p.name);
|
|
129
|
+
fs.copyFileSync(p.sourcePath, dest);
|
|
130
|
+
personaManifest.push({
|
|
131
|
+
path: `personas/${p.name}`,
|
|
132
|
+
bytes: fs.statSync(dest).size,
|
|
133
|
+
sha256: sha256OfFile(dest),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// path-protect.yaml (real data, if present)
|
|
138
|
+
const pp = pathProtectPath(sourceRoot);
|
|
139
|
+
let pathProtectManifest = null;
|
|
140
|
+
if (pp) {
|
|
141
|
+
const dest = path.join(stagingRoot, "path-protect.yaml");
|
|
142
|
+
fs.copyFileSync(pp, dest);
|
|
143
|
+
pathProtectManifest = {
|
|
144
|
+
path: "path-protect.yaml",
|
|
145
|
+
bytes: fs.statSync(dest).size,
|
|
146
|
+
sha256: sha256OfFile(dest),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// usage/history.jsonl (real data from ai/telemetry/usage-ledger.jsonl, if present)
|
|
151
|
+
// Schema is the LedgerEntry dataclass in scripts/usage_ledger.py (~line 92):
|
|
152
|
+
// ts, task_id, agent, model, model_tier, tokens_input/output/total, cost_usd, plan, ...
|
|
153
|
+
const ul = usageLedgerPath(sourceRoot);
|
|
154
|
+
let usageLedgerManifest = null;
|
|
155
|
+
if (ul) {
|
|
156
|
+
const usageOutDir = path.join(stagingRoot, "usage");
|
|
157
|
+
fs.mkdirSync(usageOutDir, { recursive: true });
|
|
158
|
+
const dest = path.join(usageOutDir, "history.jsonl");
|
|
159
|
+
fs.copyFileSync(ul, dest);
|
|
160
|
+
usageLedgerManifest = {
|
|
161
|
+
path: "usage/history.jsonl",
|
|
162
|
+
bytes: fs.statSync(dest).size,
|
|
163
|
+
sha256: sha256OfFile(dest),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 6 placeholder surfaces
|
|
168
|
+
const placeholderManifest = [];
|
|
169
|
+
for (const [surface, relPath] of PLACEHOLDER_SURFACES) {
|
|
170
|
+
const dest = path.join(stagingRoot, relPath);
|
|
171
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
172
|
+
const payload = placeholderPayload(surface);
|
|
173
|
+
fs.writeFileSync(dest, payload, "utf8");
|
|
174
|
+
placeholderManifest.push({
|
|
175
|
+
path: relPath,
|
|
176
|
+
bytes: Buffer.byteLength(payload, "utf8"),
|
|
177
|
+
sha256: sha256OfBytes(Buffer.from(payload, "utf8")),
|
|
178
|
+
status: "not-implemented",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// MANIFEST.json (must come last so other files' checksums are settled)
|
|
183
|
+
const manifest = {
|
|
184
|
+
schema_version: SCHEMA_VERSION,
|
|
185
|
+
generated_at: new Date().toISOString(),
|
|
186
|
+
since: PHASE,
|
|
187
|
+
tenant_id: tenantId,
|
|
188
|
+
files: [
|
|
189
|
+
{ path: "README.md", bytes: readmeBytes.length, sha256: sha256OfBytes(readmeBytes) },
|
|
190
|
+
{ path: "tenant.json", bytes: tenantBytes.length, sha256: sha256OfBytes(tenantBytes) },
|
|
191
|
+
...personaManifest,
|
|
192
|
+
...(pathProtectManifest ? [pathProtectManifest] : []),
|
|
193
|
+
...(usageLedgerManifest ? [usageLedgerManifest] : []),
|
|
194
|
+
...placeholderManifest,
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
fs.writeFileSync(
|
|
198
|
+
path.join(stagingRoot, "MANIFEST.json"),
|
|
199
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
200
|
+
"utf8",
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Create gzipped tarball.
|
|
204
|
+
const r = spawnSync("tar", ["-czf", path.resolve(outputPath), "-C", stagingRoot, "."]);
|
|
205
|
+
if (r.status !== 0) {
|
|
206
|
+
const stderr = r.stderr ? r.stderr.toString("utf8") : "(no stderr)";
|
|
207
|
+
throw new Error(`tar exited ${r.status}: ${stderr}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// F14 G4 Phase 3 — try cosign keyless signing. Falls back to
|
|
211
|
+
// empty .sig + .crt stubs (preserving the layout contract)
|
|
212
|
+
// when cosign is absent or the operator opts out.
|
|
213
|
+
const sigPath = `${outputPath}.sig`;
|
|
214
|
+
const crtPath = `${outputPath}.crt`;
|
|
215
|
+
const signResult = trySignTarball({
|
|
216
|
+
tarballPath: path.resolve(outputPath),
|
|
217
|
+
sigPath,
|
|
218
|
+
crtPath,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
tarballPath: path.resolve(outputPath),
|
|
223
|
+
sigPath,
|
|
224
|
+
crtPath,
|
|
225
|
+
sha256: sha256OfFile(outputPath),
|
|
226
|
+
signed: signResult.signed,
|
|
227
|
+
signSkipReason: signResult.skipReason,
|
|
228
|
+
counts: {
|
|
229
|
+
personas: personaManifest.length,
|
|
230
|
+
pathProtect: pathProtectManifest ? 1 : 0,
|
|
231
|
+
usageLedger: usageLedgerManifest ? 1 : 0,
|
|
232
|
+
placeholders: placeholderManifest.length,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
} finally {
|
|
236
|
+
// Best-effort cleanup; ignore errors.
|
|
237
|
+
try { fs.rmSync(stagingRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function trySignTarball({ tarballPath, sigPath, crtPath }) {
|
|
242
|
+
// F14 G4 Phase 3. Trust anchor matches the F16 G2 release-signing
|
|
243
|
+
// pipeline (release-artifact-signing.yml from PR #3521).
|
|
244
|
+
//
|
|
245
|
+
// Skipped when:
|
|
246
|
+
// - ODAI_EXPORT_SKIP_SIGN=1 is set (explicit opt-out)
|
|
247
|
+
// - cosign binary is not on PATH (local dev without sigstore)
|
|
248
|
+
// - cosign exits non-zero (Fulcio OIDC unavailable, etc.)
|
|
249
|
+
// In every skip case we write empty .sig + .crt stubs so the
|
|
250
|
+
// tarball layout contract from F14 G4 Phase 1 (#3569) holds.
|
|
251
|
+
if (process.env.ODAI_EXPORT_SKIP_SIGN === "1") {
|
|
252
|
+
fs.writeFileSync(sigPath, "");
|
|
253
|
+
fs.writeFileSync(crtPath, "");
|
|
254
|
+
return { signed: false, skipReason: "ODAI_EXPORT_SKIP_SIGN=1" };
|
|
255
|
+
}
|
|
256
|
+
// Probe for cosign on PATH without bombing the export when absent.
|
|
257
|
+
const probe = spawnSync("cosign", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
258
|
+
if (probe.error || probe.status !== 0) {
|
|
259
|
+
fs.writeFileSync(sigPath, "");
|
|
260
|
+
fs.writeFileSync(crtPath, "");
|
|
261
|
+
return { signed: false, skipReason: "cosign not on PATH" };
|
|
262
|
+
}
|
|
263
|
+
const env = { ...process.env, COSIGN_EXPERIMENTAL: process.env.COSIGN_EXPERIMENTAL || "1" };
|
|
264
|
+
const r = spawnSync(
|
|
265
|
+
"cosign",
|
|
266
|
+
[
|
|
267
|
+
"sign-blob",
|
|
268
|
+
"--yes",
|
|
269
|
+
"--output-signature", sigPath,
|
|
270
|
+
"--output-certificate", crtPath,
|
|
271
|
+
tarballPath,
|
|
272
|
+
],
|
|
273
|
+
{ env, stdio: ["ignore", "pipe", "pipe"] },
|
|
274
|
+
);
|
|
275
|
+
if (r.status !== 0) {
|
|
276
|
+
// Leave (or write) empty stubs so the layout still holds.
|
|
277
|
+
try { fs.writeFileSync(sigPath, ""); } catch { /* ignore */ }
|
|
278
|
+
try { fs.writeFileSync(crtPath, ""); } catch { /* ignore */ }
|
|
279
|
+
const stderr = r.stderr ? r.stderr.toString("utf8").trim().slice(0, 200) : `exit ${r.status}`;
|
|
280
|
+
return { signed: false, skipReason: `cosign sign-blob failed: ${stderr}` };
|
|
281
|
+
}
|
|
282
|
+
return { signed: true, skipReason: null };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { buildExportTarball, listPersonas, pathProtectPath, usageLedgerPath, trySignTarball, PLACEHOLDER_SURFACES, PHASE, SCHEMA_VERSION };
|
package/lib/utils/identity.js
CHANGED
|
@@ -148,6 +148,19 @@ function detectStackHint(projectFiles, manifestContents) {
|
|
|
148
148
|
return "unknown";
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Return a {filename: sha256hex} map for the given manifest contents.
|
|
153
|
+
* The raw content never leaves the machine — only the hash is exported.
|
|
154
|
+
*/
|
|
155
|
+
function hashManifestFiles(manifestContents) {
|
|
156
|
+
const crypto = require("crypto");
|
|
157
|
+
const hashes = {};
|
|
158
|
+
for (const [name, content] of Object.entries(manifestContents)) {
|
|
159
|
+
hashes[name] = crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
160
|
+
}
|
|
161
|
+
return hashes;
|
|
162
|
+
}
|
|
163
|
+
|
|
151
164
|
function collectMetadata(target) {
|
|
152
165
|
const projectFiles = [];
|
|
153
166
|
const manifestContents = {};
|
|
@@ -201,5 +214,5 @@ module.exports = {
|
|
|
201
214
|
deviceFingerprint, registerProject, projectIdFor, projectIdSeed,
|
|
202
215
|
scrubRemoteUrl, readProjectManifest, readDiscovery,
|
|
203
216
|
getGitRemoteOrigin, inferProjectName, detectStackHint,
|
|
204
|
-
collectMetadata, buildProjectIdentity,
|
|
217
|
+
collectMetadata, buildProjectIdentity, hashManifestFiles,
|
|
205
218
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0dai-dev/cli",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "One config layer for seven AI coding agents
|
|
3
|
+
"version": "4.3.6",
|
|
4
|
+
"description": "One config layer for seven AI coding agents \u2014 Claude Code, Codex, OpenCode, Gemini, Aider, Qoder",
|
|
5
5
|
"bin": {
|
|
6
6
|
"0dai": "./bin/0dai.js"
|
|
7
7
|
},
|