@enbox/gitd 0.1.0 → 0.3.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/dist/esm/cli/agent.js +66 -11
- package/dist/esm/cli/agent.js.map +1 -1
- package/dist/esm/cli/commands/auth.js +265 -0
- package/dist/esm/cli/commands/auth.js.map +1 -0
- package/dist/esm/cli/commands/migrate.js +151 -6
- package/dist/esm/cli/commands/migrate.js.map +1 -1
- package/dist/esm/cli/main.js +72 -2
- package/dist/esm/cli/main.js.map +1 -1
- package/dist/esm/git-remote/credential-main.js +34 -8
- package/dist/esm/git-remote/credential-main.js.map +1 -1
- package/dist/esm/profiles/config.js +156 -0
- package/dist/esm/profiles/config.js.map +1 -0
- package/dist/types/cli/agent.d.ts +27 -4
- package/dist/types/cli/agent.d.ts.map +1 -1
- package/dist/types/cli/commands/auth.d.ts +23 -0
- package/dist/types/cli/commands/auth.d.ts.map +1 -0
- package/dist/types/cli/commands/migrate.d.ts.map +1 -1
- package/dist/types/git-remote/credential-main.d.ts +2 -1
- package/dist/types/git-remote/credential-main.d.ts.map +1 -1
- package/dist/types/profiles/config.d.ts +69 -0
- package/dist/types/profiles/config.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/cli/agent.ts +95 -11
- package/src/cli/commands/auth.ts +290 -0
- package/src/cli/commands/migrate.ts +166 -6
- package/src/cli/main.ts +78 -2
- package/src/git-remote/credential-main.ts +41 -8
- package/src/profiles/config.ts +188 -0
|
@@ -23,8 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
import type { AgentContext } from '../agent.js';
|
|
25
25
|
|
|
26
|
+
import { createFullBundle } from '../../git-server/bundle-sync.js';
|
|
27
|
+
import { flagValue } from '../flags.js';
|
|
28
|
+
import { getRepoContext } from '../repo-context.js';
|
|
26
29
|
import { getRepoContextId } from '../repo-context.js';
|
|
27
|
-
import {
|
|
30
|
+
import { GitBackend } from '../../git-server/git-backend.js';
|
|
31
|
+
import { readGitRefs } from '../../git-server/ref-sync.js';
|
|
32
|
+
import { readFile, unlink } from 'node:fs/promises';
|
|
33
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
28
34
|
|
|
29
35
|
// ---------------------------------------------------------------------------
|
|
30
36
|
// GitHub auth — resolve a token from env or the gh CLI
|
|
@@ -63,7 +69,7 @@ export function resolveGitHubToken(): string | undefined {
|
|
|
63
69
|
try {
|
|
64
70
|
const result = spawnSync('gh', ['auth', 'token'], {
|
|
65
71
|
stdio : ['pipe', 'pipe', 'pipe'],
|
|
66
|
-
timeout :
|
|
72
|
+
timeout : 2_000,
|
|
67
73
|
});
|
|
68
74
|
const token = result.stdout?.toString().trim();
|
|
69
75
|
if (result.status === 0 && token) {
|
|
@@ -230,7 +236,7 @@ export async function migrateCommand(ctx: AgentContext, args: string[]): Promise
|
|
|
230
236
|
case 'pulls': return migratePulls(ctx, rest);
|
|
231
237
|
case 'releases': return migrateReleases(ctx, rest);
|
|
232
238
|
default:
|
|
233
|
-
console.error('Usage: gitd migrate <all|repo|issues|pulls|releases>
|
|
239
|
+
console.error('Usage: gitd migrate <all|repo|issues|pulls|releases> [owner/repo] [--repos <path>]');
|
|
234
240
|
process.exit(1);
|
|
235
241
|
}
|
|
236
242
|
}
|
|
@@ -327,8 +333,18 @@ function prependAuthor(body: string, ghLogin: string): string {
|
|
|
327
333
|
// migrate all
|
|
328
334
|
// ---------------------------------------------------------------------------
|
|
329
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Resolve the repos base path from CLI flags or environment.
|
|
338
|
+
* Returns `null` when no explicit path was provided, so callers can
|
|
339
|
+
* decide whether to skip git content migration.
|
|
340
|
+
*/
|
|
341
|
+
function resolveReposPath(args: string[]): string | null {
|
|
342
|
+
return flagValue(args, '--repos') ?? process.env.GITD_REPOS ?? null;
|
|
343
|
+
}
|
|
344
|
+
|
|
330
345
|
async function migrateAll(ctx: AgentContext, args: string[]): Promise<void> {
|
|
331
346
|
const { owner, repo } = resolveGhRepo(args);
|
|
347
|
+
const reposPath = resolveReposPath(args);
|
|
332
348
|
const slug = `${owner}/${repo}`;
|
|
333
349
|
|
|
334
350
|
const token = resolveGitHubToken();
|
|
@@ -343,13 +359,26 @@ async function migrateAll(ctx: AgentContext, args: string[]): Promise<void> {
|
|
|
343
359
|
// Step 1: repo metadata.
|
|
344
360
|
await migrateRepoInner(ctx, owner, repo);
|
|
345
361
|
|
|
346
|
-
// Step 2:
|
|
362
|
+
// Step 2: git content (clone, bundle, refs).
|
|
363
|
+
if (reposPath) {
|
|
364
|
+
try {
|
|
365
|
+
await migrateGitContent(ctx, owner, repo, reposPath);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.error(` Warning: git content migration failed: ${(err as Error).message}`);
|
|
368
|
+
console.error(' Metadata, issues, PRs, and releases will still be imported.');
|
|
369
|
+
console.error(' Re-run with a valid --repos path to retry git content migration.\n');
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
console.log(' Skipping git content — pass --repos <path> or set GITD_REPOS to include git data.');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Step 3: issues + comments.
|
|
347
376
|
const issueCount = await migrateIssuesInner(ctx, owner, repo);
|
|
348
377
|
|
|
349
|
-
// Step
|
|
378
|
+
// Step 4: pull requests + reviews.
|
|
350
379
|
const pullCount = await migratePullsInner(ctx, owner, repo);
|
|
351
380
|
|
|
352
|
-
// Step
|
|
381
|
+
// Step 5: releases.
|
|
353
382
|
const releaseCount = await migrateReleasesInner(ctx, owner, repo);
|
|
354
383
|
|
|
355
384
|
console.log(`\nMigration complete: ${slug}`);
|
|
@@ -368,8 +397,14 @@ async function migrateAll(ctx: AgentContext, args: string[]): Promise<void> {
|
|
|
368
397
|
|
|
369
398
|
async function migrateRepo(ctx: AgentContext, args: string[]): Promise<void> {
|
|
370
399
|
const { owner, repo } = resolveGhRepo(args);
|
|
400
|
+
const reposPath = resolveReposPath(args);
|
|
371
401
|
try {
|
|
372
402
|
await migrateRepoInner(ctx, owner, repo);
|
|
403
|
+
if (reposPath) {
|
|
404
|
+
await migrateGitContent(ctx, owner, repo, reposPath);
|
|
405
|
+
} else {
|
|
406
|
+
console.log(' Skipping git content — pass --repos <path> or set GITD_REPOS to include git data.');
|
|
407
|
+
}
|
|
373
408
|
} catch (err) {
|
|
374
409
|
console.error(`Failed to migrate repo: ${(err as Error).message}`);
|
|
375
410
|
process.exit(1);
|
|
@@ -413,6 +448,131 @@ async function migrateRepoInner(ctx: AgentContext, owner: string, repo: string):
|
|
|
413
448
|
console.log(` Source: ${gh.html_url}`);
|
|
414
449
|
}
|
|
415
450
|
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// migrate git content (clone + bundle + refs)
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Clone the GitHub repository as a bare repo, create a full git bundle,
|
|
457
|
+
* upload it to DWN, and sync all refs to DWN records.
|
|
458
|
+
*
|
|
459
|
+
* This is the "git content" migration step that turns the metadata-only
|
|
460
|
+
* repo record into a fully cloneable repo.
|
|
461
|
+
*/
|
|
462
|
+
async function migrateGitContent(
|
|
463
|
+
ctx: AgentContext,
|
|
464
|
+
owner: string,
|
|
465
|
+
repo: string,
|
|
466
|
+
reposPath: string,
|
|
467
|
+
): Promise<void> {
|
|
468
|
+
const slug = `${owner}/${repo}`;
|
|
469
|
+
console.log(`Importing git content from ${slug}...`);
|
|
470
|
+
|
|
471
|
+
const backend = new GitBackend({ basePath: reposPath });
|
|
472
|
+
const repoPath = backend.repoPath(ctx.did, repo);
|
|
473
|
+
|
|
474
|
+
// Step 1: Clone bare repo from GitHub (or skip if already on disk).
|
|
475
|
+
if (backend.exists(ctx.did, repo)) {
|
|
476
|
+
console.log(` Bare repo already exists at ${repoPath} — skipping clone.`);
|
|
477
|
+
} else {
|
|
478
|
+
const cloneUrl = buildCloneUrl(owner, repo);
|
|
479
|
+
console.log(` Cloning ${cloneUrl} → ${repoPath}`);
|
|
480
|
+
await cloneBare(cloneUrl, repoPath);
|
|
481
|
+
console.log(' Clone complete.');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Step 2: Create full git bundle.
|
|
485
|
+
console.log(' Creating git bundle...');
|
|
486
|
+
const bundleInfo = await createFullBundle(repoPath);
|
|
487
|
+
console.log(` Bundle: ${bundleInfo.size} bytes, ${bundleInfo.refCount} ref(s), tip: ${bundleInfo.tipCommit.slice(0, 8)}`);
|
|
488
|
+
|
|
489
|
+
// Step 3: Upload bundle to DWN.
|
|
490
|
+
const { contextId: repoContextId, visibility } = await getRepoContext(ctx);
|
|
491
|
+
const encrypt = visibility === 'private';
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const bundleData = new Uint8Array(await readFile(bundleInfo.path));
|
|
495
|
+
|
|
496
|
+
const { status } = await ctx.repo.records.create('repo/bundle', {
|
|
497
|
+
data : bundleData,
|
|
498
|
+
dataFormat : 'application/x-git-bundle',
|
|
499
|
+
tags : {
|
|
500
|
+
tipCommit : bundleInfo.tipCommit,
|
|
501
|
+
isFull : true,
|
|
502
|
+
refCount : bundleInfo.refCount,
|
|
503
|
+
size : bundleInfo.size,
|
|
504
|
+
},
|
|
505
|
+
parentContextId : repoContextId,
|
|
506
|
+
encryption : encrypt,
|
|
507
|
+
} as any);
|
|
508
|
+
|
|
509
|
+
if (status.code >= 300) {
|
|
510
|
+
throw new Error(`Failed to create bundle record: ${status.code} ${status.detail}`);
|
|
511
|
+
}
|
|
512
|
+
console.log(' Bundle uploaded to DWN.');
|
|
513
|
+
} finally {
|
|
514
|
+
await unlink(bundleInfo.path).catch(() => {});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Step 4: Sync git refs to DWN.
|
|
518
|
+
console.log(' Syncing refs to DWN...');
|
|
519
|
+
const gitRefs = await readGitRefs(repoPath);
|
|
520
|
+
|
|
521
|
+
let refCount = 0;
|
|
522
|
+
for (const ref of gitRefs) {
|
|
523
|
+
const { status } = await ctx.refs.records.create('repo/ref', {
|
|
524
|
+
data : { name: ref.name, target: ref.target, type: ref.type },
|
|
525
|
+
tags : { name: ref.name, type: ref.type, target: ref.target },
|
|
526
|
+
parentContextId : repoContextId,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (status.code >= 300) {
|
|
530
|
+
console.error(` Failed to sync ref ${ref.name}: ${status.code} ${status.detail}`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
refCount++;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
console.log(` Synced ${refCount} ref(s) to DWN.`);
|
|
537
|
+
console.log(` Git content migration complete.`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Build the clone URL for a GitHub repository.
|
|
542
|
+
* Uses the token (if available) for authenticated HTTPS clone.
|
|
543
|
+
*/
|
|
544
|
+
function buildCloneUrl(owner: string, repo: string): string {
|
|
545
|
+
const token = resolveGitHubToken();
|
|
546
|
+
if (token) {
|
|
547
|
+
return `https://${token}@github.com/${owner}/${repo}.git`;
|
|
548
|
+
}
|
|
549
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Clone a git repository as a bare repo using `git clone --bare`.
|
|
554
|
+
*/
|
|
555
|
+
function cloneBare(url: string, destPath: string): Promise<void> {
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const child = spawn('git', ['clone', '--bare', url, destPath], {
|
|
558
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const stderrChunks: Buffer[] = [];
|
|
562
|
+
child.stderr!.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
563
|
+
|
|
564
|
+
child.on('error', reject);
|
|
565
|
+
child.on('exit', (code: number | null) => {
|
|
566
|
+
if (code !== 0) {
|
|
567
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf-8');
|
|
568
|
+
reject(new Error(`git clone --bare failed (exit ${code}): ${stderr}`));
|
|
569
|
+
} else {
|
|
570
|
+
resolve();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
416
576
|
// ---------------------------------------------------------------------------
|
|
417
577
|
// migrate issues
|
|
418
578
|
// ---------------------------------------------------------------------------
|
package/src/cli/main.ts
CHANGED
|
@@ -66,10 +66,13 @@
|
|
|
66
66
|
* @module
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
|
+
import { authCommand } from './commands/auth.js';
|
|
69
70
|
import { ciCommand } from './commands/ci.js';
|
|
70
71
|
import { cloneCommand } from './commands/clone.js';
|
|
71
72
|
import { connectAgent } from './agent.js';
|
|
73
|
+
import { createRequire } from 'node:module';
|
|
72
74
|
import { daemonCommand } from './commands/daemon.js';
|
|
75
|
+
import { flagValue } from './flags.js';
|
|
73
76
|
import { githubApiCommand } from './commands/github-api.js';
|
|
74
77
|
import { indexerCommand } from '../indexer/main.js';
|
|
75
78
|
import { initCommand } from './commands/init.js';
|
|
@@ -88,6 +91,7 @@ import { shimCommand } from './commands/shim.js';
|
|
|
88
91
|
import { socialCommand } from './commands/social.js';
|
|
89
92
|
import { webCommand } from './commands/web.js';
|
|
90
93
|
import { wikiCommand } from './commands/wiki.js';
|
|
94
|
+
import { profileDataPath, resolveProfile } from '../profiles/config.js';
|
|
91
95
|
|
|
92
96
|
// ---------------------------------------------------------------------------
|
|
93
97
|
// Arg parsing
|
|
@@ -104,6 +108,11 @@ const rest = args.slice(1);
|
|
|
104
108
|
function printUsage(): void {
|
|
105
109
|
console.log('gitd — decentralized forge powered by DWN protocols\n');
|
|
106
110
|
console.log('Commands:');
|
|
111
|
+
console.log(' auth Show current identity info');
|
|
112
|
+
console.log(' auth login Create or import an identity');
|
|
113
|
+
console.log(' auth list List all profiles');
|
|
114
|
+
console.log(' auth use <profile> [--global] Set active profile');
|
|
115
|
+
console.log('');
|
|
107
116
|
console.log(' setup Configure git for DID-based remotes');
|
|
108
117
|
console.log(' clone <did>/<repo> Clone a repository via DID');
|
|
109
118
|
console.log(' init <name> Create a repo record + bare git repo');
|
|
@@ -216,8 +225,49 @@ async function getPassword(): Promise<string> {
|
|
|
216
225
|
const env = process.env.GITD_PASSWORD;
|
|
217
226
|
if (env) { return env; }
|
|
218
227
|
|
|
219
|
-
// Interactive prompt
|
|
228
|
+
// Interactive prompt — hide input when running in a TTY.
|
|
220
229
|
process.stdout.write('Vault password: ');
|
|
230
|
+
|
|
231
|
+
if (process.stdin.isTTY) {
|
|
232
|
+
// Raw mode: read character-by-character, echo nothing.
|
|
233
|
+
const password = await new Promise<string>((resolve) => {
|
|
234
|
+
let buf = '';
|
|
235
|
+
process.stdin.setRawMode(true);
|
|
236
|
+
process.stdin.setEncoding('utf8');
|
|
237
|
+
process.stdin.resume();
|
|
238
|
+
|
|
239
|
+
const onData = (ch: string): void => {
|
|
240
|
+
const code = ch.charCodeAt(0);
|
|
241
|
+
|
|
242
|
+
if (ch === '\r' || ch === '\n') {
|
|
243
|
+
// Enter — done.
|
|
244
|
+
process.stdin.setRawMode(false);
|
|
245
|
+
process.stdin.pause();
|
|
246
|
+
process.stdin.removeListener('data', onData);
|
|
247
|
+
process.stdout.write('\n');
|
|
248
|
+
resolve(buf);
|
|
249
|
+
} else if (code === 3) {
|
|
250
|
+
// Ctrl-C — abort.
|
|
251
|
+
process.stdin.setRawMode(false);
|
|
252
|
+
process.stdout.write('\n');
|
|
253
|
+
process.exit(130);
|
|
254
|
+
} else if (code === 127 || code === 8) {
|
|
255
|
+
// Backspace / Delete.
|
|
256
|
+
if (buf.length > 0) {
|
|
257
|
+
buf = buf.slice(0, -1);
|
|
258
|
+
}
|
|
259
|
+
} else if (code >= 32) {
|
|
260
|
+
// Printable character.
|
|
261
|
+
buf += ch;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
process.stdin.on('data', onData);
|
|
266
|
+
});
|
|
267
|
+
return password;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Non-TTY fallback (piped input).
|
|
221
271
|
const response = await new Promise<string>((resolve) => {
|
|
222
272
|
let buf = '';
|
|
223
273
|
process.stdin.setEncoding('utf8');
|
|
@@ -234,7 +284,18 @@ async function getPassword(): Promise<string> {
|
|
|
234
284
|
// Main
|
|
235
285
|
// ---------------------------------------------------------------------------
|
|
236
286
|
|
|
287
|
+
function printVersion(): void {
|
|
288
|
+
const require = createRequire(import.meta.url);
|
|
289
|
+
const pkg = require('../../package.json') as { version: string };
|
|
290
|
+
console.log(`gitd ${pkg.version}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
237
293
|
async function main(): Promise<void> {
|
|
294
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
295
|
+
printVersion();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
238
299
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
239
300
|
printUsage();
|
|
240
301
|
return;
|
|
@@ -249,11 +310,19 @@ async function main(): Promise<void> {
|
|
|
249
310
|
case 'clone':
|
|
250
311
|
await cloneCommand(rest);
|
|
251
312
|
return;
|
|
313
|
+
|
|
314
|
+
case 'auth':
|
|
315
|
+
// Auth can run without a pre-existing profile (for `login`).
|
|
316
|
+
await authCommand(null, rest);
|
|
317
|
+
return;
|
|
252
318
|
}
|
|
253
319
|
|
|
254
320
|
// Commands that require the Web5 agent.
|
|
255
321
|
const password = await getPassword();
|
|
256
|
-
const
|
|
322
|
+
const profileFlag = flagValue(rest, '--profile');
|
|
323
|
+
const profileName = resolveProfile(profileFlag);
|
|
324
|
+
const dataPath = profileName ? profileDataPath(profileName) : undefined;
|
|
325
|
+
const ctx = await connectAgent({ password, dataPath });
|
|
257
326
|
|
|
258
327
|
switch (command) {
|
|
259
328
|
case 'init':
|
|
@@ -342,6 +411,13 @@ async function main(): Promise<void> {
|
|
|
342
411
|
printUsage();
|
|
343
412
|
process.exit(1);
|
|
344
413
|
}
|
|
414
|
+
|
|
415
|
+
// One-shot commands reach here after completing. The Web5 agent keeps
|
|
416
|
+
// LevelDB stores and other handles open, which prevents the process from
|
|
417
|
+
// exiting naturally. Long-running commands (serve, web, daemon, indexer,
|
|
418
|
+
// github-api, shim) never reach this point because they block on an
|
|
419
|
+
// infinite promise internally.
|
|
420
|
+
process.exit(0);
|
|
345
421
|
}
|
|
346
422
|
|
|
347
423
|
main().catch((err: Error) => {
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
* helper = /path/to/git-remote-did-credential
|
|
17
17
|
*
|
|
18
18
|
* Environment:
|
|
19
|
-
* GITD_PASSWORD
|
|
19
|
+
* GITD_PASSWORD — vault password for the local agent
|
|
20
|
+
* ENBOX_PROFILE — (optional) profile name override
|
|
20
21
|
*
|
|
21
22
|
* @module
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
|
-
import {
|
|
25
|
+
import type { BearerDid } from '@enbox/dids';
|
|
26
|
+
|
|
27
|
+
import { Web5UserAgent } from '@enbox/agent';
|
|
25
28
|
|
|
26
29
|
import {
|
|
27
30
|
createPushTokenPayload,
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
formatCredentialResponse,
|
|
34
37
|
parseCredentialRequest,
|
|
35
38
|
} from './credential-helper.js';
|
|
39
|
+
import { profileDataPath, resolveProfile } from '../profiles/config.js';
|
|
36
40
|
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
38
42
|
// Main
|
|
@@ -63,10 +67,7 @@ async function main(): Promise<void> {
|
|
|
63
67
|
return;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
const {
|
|
67
|
-
password,
|
|
68
|
-
sync: 'off',
|
|
69
|
-
});
|
|
70
|
+
const { did, bearerDid } = await connectForCredentials(password);
|
|
70
71
|
|
|
71
72
|
// Extract the owner DID and repo from the URL path.
|
|
72
73
|
const path = request.path ?? '';
|
|
@@ -82,8 +83,8 @@ async function main(): Promise<void> {
|
|
|
82
83
|
const payload = createPushTokenPayload(did, ownerDid, repo);
|
|
83
84
|
const token = encodePushToken(payload);
|
|
84
85
|
|
|
85
|
-
// Sign the token using the
|
|
86
|
-
const signer = await
|
|
86
|
+
// Sign the token using the identity's DID signer (not the agent DID).
|
|
87
|
+
const signer = await bearerDid.getSigner();
|
|
87
88
|
const tokenBytes = new TextEncoder().encode(token);
|
|
88
89
|
const signature = await signer.sign({ data: tokenBytes });
|
|
89
90
|
const signatureBase64url = Buffer.from(signature).toString('base64url');
|
|
@@ -96,6 +97,38 @@ async function main(): Promise<void> {
|
|
|
96
97
|
process.stdout.write(formatCredentialResponse(creds));
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Agent connection
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Connect to the Web5 agent and return the identity DID and its BearerDid.
|
|
106
|
+
*
|
|
107
|
+
* Resolves the active profile (env, git config, global default, or single
|
|
108
|
+
* fallback) and connects using the profile's agent data path. Falls back
|
|
109
|
+
* to the legacy CWD-relative `DATA/AGENT` path when no profile exists.
|
|
110
|
+
*/
|
|
111
|
+
async function connectForCredentials(
|
|
112
|
+
password: string,
|
|
113
|
+
): Promise<{ did: string; bearerDid: BearerDid }> {
|
|
114
|
+
// Resolve profile (env, git config, global default, single fallback).
|
|
115
|
+
// When a profile exists, the agent lives at ~/.enbox/profiles/<name>/DATA/AGENT.
|
|
116
|
+
// Otherwise, fall back to the CWD-relative default path (legacy).
|
|
117
|
+
const profileName = resolveProfile();
|
|
118
|
+
const dataPath = profileName ? profileDataPath(profileName) : undefined;
|
|
119
|
+
|
|
120
|
+
const agent = await Web5UserAgent.create(dataPath ? { dataPath } : undefined);
|
|
121
|
+
await agent.start({ password });
|
|
122
|
+
|
|
123
|
+
const identities = await agent.identity.list();
|
|
124
|
+
const identity = identities[0];
|
|
125
|
+
if (!identity) {
|
|
126
|
+
throw new Error('No identity found in agent');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { did: identity.did.uri, bearerDid: identity.did };
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
// ---------------------------------------------------------------------------
|
|
100
133
|
// Helpers
|
|
101
134
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile configuration — persistent config at `~/.enbox/config.json`.
|
|
3
|
+
*
|
|
4
|
+
* Manages the set of named identity profiles and the default selection.
|
|
5
|
+
* This module handles reading, writing, and resolving profiles across
|
|
6
|
+
* multiple selection sources (flag, env, git config, global default).
|
|
7
|
+
*
|
|
8
|
+
* Storage layout:
|
|
9
|
+
* ~/.enbox/
|
|
10
|
+
* config.json Global config (this module)
|
|
11
|
+
* profiles/
|
|
12
|
+
* <name>/DATA/AGENT/... Per-profile Web5 agent stores
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Base directory for all enbox data. Override with `ENBOX_HOME`. */
|
|
27
|
+
export function enboxHome(): string {
|
|
28
|
+
return process.env.ENBOX_HOME ?? join(homedir(), '.enbox');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Path to the global config file. */
|
|
32
|
+
export function configPath(): string {
|
|
33
|
+
return join(enboxHome(), 'config.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Base directory for all profile agent data. */
|
|
37
|
+
export function profilesDir(): string {
|
|
38
|
+
return join(enboxHome(), 'profiles');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Path to a specific profile's agent data directory. */
|
|
42
|
+
export function profileDataPath(name: string): string {
|
|
43
|
+
return join(profilesDir(), name, 'DATA', 'AGENT');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Config types
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/** Metadata about a single profile stored in config.json. */
|
|
51
|
+
export type ProfileEntry = {
|
|
52
|
+
/** Display name for the profile. */
|
|
53
|
+
name : string;
|
|
54
|
+
/** The DID URI associated with this profile. */
|
|
55
|
+
did : string;
|
|
56
|
+
/** ISO 8601 timestamp when the profile was created. */
|
|
57
|
+
createdAt : string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Top-level config.json shape. */
|
|
61
|
+
export type EnboxConfig = {
|
|
62
|
+
/** Schema version for forward-compatibility. */
|
|
63
|
+
version : number;
|
|
64
|
+
/** Name of the default profile. */
|
|
65
|
+
defaultProfile : string;
|
|
66
|
+
/** Map of profile name → metadata. */
|
|
67
|
+
profiles : Record<string, ProfileEntry>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Config I/O
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/** Read the global config. Returns a default if the file doesn't exist. */
|
|
75
|
+
export function readConfig(): EnboxConfig {
|
|
76
|
+
const path = configPath();
|
|
77
|
+
if (!existsSync(path)) {
|
|
78
|
+
return { version: 1, defaultProfile: '', profiles: {} };
|
|
79
|
+
}
|
|
80
|
+
const raw = readFileSync(path, 'utf-8');
|
|
81
|
+
return JSON.parse(raw) as EnboxConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Write the global config atomically. */
|
|
85
|
+
export function writeConfig(config: EnboxConfig): void {
|
|
86
|
+
const path = configPath();
|
|
87
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
88
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Add or update a profile entry in the global config. */
|
|
92
|
+
export function upsertProfile(name: string, entry: ProfileEntry): void {
|
|
93
|
+
const config = readConfig();
|
|
94
|
+
config.profiles[name] = entry;
|
|
95
|
+
|
|
96
|
+
// If this is the first profile, make it the default.
|
|
97
|
+
if (!config.defaultProfile || Object.keys(config.profiles).length === 1) {
|
|
98
|
+
config.defaultProfile = name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writeConfig(config);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Remove a profile entry from the global config. */
|
|
105
|
+
export function removeProfile(name: string): void {
|
|
106
|
+
const config = readConfig();
|
|
107
|
+
delete config.profiles[name];
|
|
108
|
+
|
|
109
|
+
// If we removed the default, pick the first remaining (or clear).
|
|
110
|
+
if (config.defaultProfile === name) {
|
|
111
|
+
const remaining = Object.keys(config.profiles);
|
|
112
|
+
config.defaultProfile = remaining[0] ?? '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeConfig(config);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** List all profile names. */
|
|
119
|
+
export function listProfiles(): string[] {
|
|
120
|
+
return Object.keys(readConfig().profiles);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Profile resolution
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve which profile to use.
|
|
129
|
+
*
|
|
130
|
+
* Precedence (highest to lowest):
|
|
131
|
+
* 1. `--profile <name>` flag (passed as `flagProfile`)
|
|
132
|
+
* 2. `ENBOX_PROFILE` environment variable
|
|
133
|
+
* 3. `.git/config` → `[enbox] profile = <name>`
|
|
134
|
+
* 4. `~/.enbox/config.json` → `defaultProfile`
|
|
135
|
+
* 5. First (and only) profile, if exactly one exists
|
|
136
|
+
*
|
|
137
|
+
* Returns `null` when no profile can be resolved (i.e. none exist).
|
|
138
|
+
*/
|
|
139
|
+
export function resolveProfile(flagProfile?: string): string | null {
|
|
140
|
+
// 1. Explicit flag.
|
|
141
|
+
if (flagProfile) { return flagProfile; }
|
|
142
|
+
|
|
143
|
+
// 2. Environment variable.
|
|
144
|
+
const envProfile = process.env.ENBOX_PROFILE;
|
|
145
|
+
if (envProfile) { return envProfile; }
|
|
146
|
+
|
|
147
|
+
// 3. Per-repo git config.
|
|
148
|
+
const gitProfile = readGitConfigProfile();
|
|
149
|
+
if (gitProfile) { return gitProfile; }
|
|
150
|
+
|
|
151
|
+
// 4. Global default.
|
|
152
|
+
const config = readConfig();
|
|
153
|
+
if (config.defaultProfile) { return config.defaultProfile; }
|
|
154
|
+
|
|
155
|
+
// 5. Single profile fallback.
|
|
156
|
+
const names = Object.keys(config.profiles);
|
|
157
|
+
if (names.length === 1) { return names[0]; }
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read the `[enbox] profile` setting from the current repo's `.git/config`.
|
|
164
|
+
* Returns `null` if not in a git repo or the setting is absent.
|
|
165
|
+
*/
|
|
166
|
+
function readGitConfigProfile(): string | null {
|
|
167
|
+
try {
|
|
168
|
+
const result = spawnSync('git', ['config', '--local', 'enbox.profile'], {
|
|
169
|
+
stdio : ['pipe', 'pipe', 'pipe'],
|
|
170
|
+
timeout : 2_000,
|
|
171
|
+
});
|
|
172
|
+
const value = result.stdout?.toString().trim();
|
|
173
|
+
if (result.status === 0 && value) { return value; }
|
|
174
|
+
} catch {
|
|
175
|
+
// Not in a git repo or git not available.
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Write the `[enbox] profile` setting to the current repo's `.git/config`.
|
|
182
|
+
*/
|
|
183
|
+
export function setGitConfigProfile(name: string): void {
|
|
184
|
+
spawnSync('git', ['config', '--local', 'enbox.profile', name], {
|
|
185
|
+
stdio : ['pipe', 'pipe', 'pipe'],
|
|
186
|
+
timeout : 2_000,
|
|
187
|
+
});
|
|
188
|
+
}
|