da-proj 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +410 -0
- package/package.json +51 -0
- package/src/README.md +190 -0
- package/src/commands/init.ts +236 -0
- package/src/commands/pull.ts +119 -0
- package/src/commands/push.ts +68 -0
- package/src/commands/secrets.ts +251 -0
- package/src/commands/setup-github-sync.ts +194 -0
- package/src/commands/sync-status.ts +131 -0
- package/src/commands/sync.ts +159 -0
- package/src/generators/mdx.ts +46 -0
- package/src/generators/readme.ts +45 -0
- package/src/generators/schema.ts +69 -0
- package/src/generators/workflow.ts +69 -0
- package/src/index.ts +98 -0
- package/src/types/index.ts +47 -0
- package/src/utils/config.ts +52 -0
- package/src/utils/github-config.ts +67 -0
- package/src/utils/github.ts +297 -0
- package/src/utils/logger.ts +20 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Generar workflow de GitHub Actions
|
|
2
|
+
export function generateWorkflow(): string {
|
|
3
|
+
return `name: Sync to Portfolio
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
paths:
|
|
9
|
+
- '.project-metadata.mdx'
|
|
10
|
+
- 'proj-images/**'
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
sync:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Bun
|
|
22
|
+
uses: oven-sh/setup-bun@v1
|
|
23
|
+
with:
|
|
24
|
+
bun-version: latest
|
|
25
|
+
|
|
26
|
+
- name: Extract metadata
|
|
27
|
+
id: metadata
|
|
28
|
+
run: |
|
|
29
|
+
bun install gray-matter
|
|
30
|
+
bun run -e "
|
|
31
|
+
import matter from 'gray-matter';
|
|
32
|
+
import { readFileSync } from 'fs';
|
|
33
|
+
|
|
34
|
+
const content = readFileSync('.project-metadata.mdx', 'utf8');
|
|
35
|
+
const { data, content: markdown } = matter(content);
|
|
36
|
+
|
|
37
|
+
data.repository = {
|
|
38
|
+
owner: process.env.GITHUB_REPOSITORY.split('/')[0],
|
|
39
|
+
name: process.env.GITHUB_REPOSITORY.split('/')[1],
|
|
40
|
+
url: \`https://github.com/\${process.env.GITHUB_REPOSITORY}\`
|
|
41
|
+
};
|
|
42
|
+
data.lastCommit = process.env.GITHUB_SHA;
|
|
43
|
+
data.lastUpdated = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
console.log('METADATA=' + JSON.stringify({ metadata: data, markdown }));
|
|
46
|
+
" > output.txt
|
|
47
|
+
|
|
48
|
+
METADATA=$(cat output.txt | grep METADATA | cut -d'=' -f2-)
|
|
49
|
+
echo "data<<EOF" >> $GITHUB_OUTPUT
|
|
50
|
+
echo "$METADATA" >> $GITHUB_OUTPUT
|
|
51
|
+
echo "EOF" >> $GITHUB_OUTPUT
|
|
52
|
+
|
|
53
|
+
- name: Notify Portfolio
|
|
54
|
+
env:
|
|
55
|
+
PORTFOLIO_API_URL: \${{ secrets.PORTFOLIO_API_URL }}
|
|
56
|
+
PORTFOLIO_API_KEY: \${{ secrets.PORTFOLIO_API_KEY }}
|
|
57
|
+
run: |
|
|
58
|
+
curl -X POST "$PORTFOLIO_API_URL/api/update-project" \\
|
|
59
|
+
-H "Content-Type: application/json" \\
|
|
60
|
+
-H "Authorization: Bearer $PORTFOLIO_API_KEY" \\
|
|
61
|
+
-d '\${{ steps.metadata.outputs.data }}'
|
|
62
|
+
|
|
63
|
+
- name: Create deployment badge
|
|
64
|
+
if: success()
|
|
65
|
+
run: |
|
|
66
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
67
|
+
echo "Last sync: $(date)" >> $GITHUB_STEP_SUMMARY
|
|
68
|
+
`;
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "util";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { pullCommand } from "./commands/pull.js";
|
|
6
|
+
import { pushCommand } from "./commands/push.js";
|
|
7
|
+
import { secretsCommand } from "./commands/secrets.js";
|
|
8
|
+
import { setupGitHubSyncCommand } from "./commands/setup-github-sync.js";
|
|
9
|
+
import { syncStatusCommand } from "./commands/sync-status.js";
|
|
10
|
+
import { syncCommand } from "./commands/sync.js";
|
|
11
|
+
import { colors } from "./utils/logger.js";
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const args = parseArgs({
|
|
15
|
+
args: Bun.argv.slice(2),
|
|
16
|
+
options: {
|
|
17
|
+
help: { type: "boolean", short: "h" },
|
|
18
|
+
init: { type: "boolean", short: "i" },
|
|
19
|
+
portfolio: { type: "string", short: "p" },
|
|
20
|
+
secrets: { type: "boolean", short: "s" },
|
|
21
|
+
sync: { type: "boolean" },
|
|
22
|
+
"setup-github-sync": { type: "boolean" },
|
|
23
|
+
push: { type: "boolean" },
|
|
24
|
+
pull: { type: "boolean" },
|
|
25
|
+
"sync-status": { type: "boolean" },
|
|
26
|
+
},
|
|
27
|
+
allowPositionals: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (args.values.help) {
|
|
31
|
+
console.log(`
|
|
32
|
+
${colors.bright}da-proj${colors.reset} - CLI tool to setup portfolio project metadata
|
|
33
|
+
|
|
34
|
+
${colors.bright}USAGE:${colors.reset}
|
|
35
|
+
bunx da-proj [options]
|
|
36
|
+
|
|
37
|
+
${colors.bright}OPTIONS:${colors.reset}
|
|
38
|
+
-i, --init Initialize project metadata
|
|
39
|
+
-s, --secrets Configure GitHub secrets (requires GitHub CLI)
|
|
40
|
+
--sync Sync configuration across computers
|
|
41
|
+
-p, --portfolio <url> Portfolio API URL
|
|
42
|
+
-h, --help Show this help message
|
|
43
|
+
|
|
44
|
+
${colors.bright}GITHUB SYNC:${colors.reset}
|
|
45
|
+
--setup-github-sync Setup GitHub repository for config sync
|
|
46
|
+
--push Upload local configuration to GitHub
|
|
47
|
+
--pull Download configuration from GitHub
|
|
48
|
+
--sync-status Check synchronization status
|
|
49
|
+
|
|
50
|
+
${colors.bright}EXAMPLES:${colors.reset}
|
|
51
|
+
bunx da-proj --init
|
|
52
|
+
bunx da-proj --secrets
|
|
53
|
+
bunx da-proj --setup-github-sync
|
|
54
|
+
bunx da-proj --push
|
|
55
|
+
bunx da-proj --pull
|
|
56
|
+
bunx da-proj --sync-status
|
|
57
|
+
`);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// GitHub sync commands
|
|
62
|
+
if (args.values["setup-github-sync"]) {
|
|
63
|
+
await setupGitHubSyncCommand();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (args.values.push) {
|
|
68
|
+
await pushCommand();
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args.values.pull) {
|
|
73
|
+
await pullCommand();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.values["sync-status"]) {
|
|
78
|
+
await syncStatusCommand();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Si se usa --secrets, configurar secrets y salir
|
|
83
|
+
if (args.values.secrets) {
|
|
84
|
+
await secretsCommand();
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Si se usa --sync, sincronizar configuración
|
|
89
|
+
if (args.values.sync) {
|
|
90
|
+
await syncCommand();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Por defecto o con --init, ejecutar comando init
|
|
95
|
+
await initCommand();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Tipos para el proyecto
|
|
2
|
+
export interface ProjectMetadata {
|
|
3
|
+
title: string;
|
|
4
|
+
category: string;
|
|
5
|
+
type: "featured" | "small";
|
|
6
|
+
status: "active" | "archived" | "in-progress";
|
|
7
|
+
age?: string;
|
|
8
|
+
repository?: string;
|
|
9
|
+
demo?: string;
|
|
10
|
+
technologies: string[];
|
|
11
|
+
images: {
|
|
12
|
+
cover: string;
|
|
13
|
+
gallery: string[];
|
|
14
|
+
};
|
|
15
|
+
industry?: string;
|
|
16
|
+
timeline?: string;
|
|
17
|
+
details?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Profile {
|
|
21
|
+
name: string;
|
|
22
|
+
portfolioUrl: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Config {
|
|
27
|
+
profiles?: Profile[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExistingSecrets {
|
|
31
|
+
url: boolean;
|
|
32
|
+
key: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// GitHub sync types
|
|
36
|
+
export interface GitHubSyncConfig {
|
|
37
|
+
repoUrl: string;
|
|
38
|
+
repoName: string;
|
|
39
|
+
repoOwner: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GitHubRepo {
|
|
43
|
+
name: string;
|
|
44
|
+
url: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
private: boolean;
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import type { Config, Profile } from "../types/index.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
// Obtener ruta del archivo de configuración global
|
|
7
|
+
export function getConfigPath(): string {
|
|
8
|
+
const homeDir = process.env.USERPROFILE || process.env.HOME || '';
|
|
9
|
+
return `${homeDir}/.da-proj-config.json`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Leer configuración global
|
|
13
|
+
export async function readGlobalConfig(): Promise<Config> {
|
|
14
|
+
try {
|
|
15
|
+
const configPath = getConfigPath();
|
|
16
|
+
if (existsSync(configPath)) {
|
|
17
|
+
const content = await readFile(configPath, 'utf-8');
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// Si hay error, retornar objeto vacío
|
|
22
|
+
}
|
|
23
|
+
return { profiles: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Guardar configuración global
|
|
27
|
+
export async function saveGlobalConfig(profile: Profile): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
const configPath = getConfigPath();
|
|
30
|
+
const config = await readGlobalConfig();
|
|
31
|
+
|
|
32
|
+
if (!config.profiles) {
|
|
33
|
+
config.profiles = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Buscar si ya existe un perfil con ese nombre
|
|
37
|
+
const existingIndex = config.profiles.findIndex(p => p.name === profile.name);
|
|
38
|
+
|
|
39
|
+
if (existingIndex >= 0) {
|
|
40
|
+
// Actualizar existente
|
|
41
|
+
config.profiles[existingIndex] = profile;
|
|
42
|
+
} else {
|
|
43
|
+
// Agregar nuevo
|
|
44
|
+
config.profiles.push(profile);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
48
|
+
log.success(`Configuration saved to ${configPath}`);
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
log.warn(`Could not save config: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for GitHub sync
|
|
6
|
+
*/
|
|
7
|
+
export interface GitHubSyncConfig {
|
|
8
|
+
repoUrl: string; // "https://github.com/user/da-proj-secrets"
|
|
9
|
+
repoName: string; // "da-proj-secrets"
|
|
10
|
+
repoOwner: string; // "user"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get path to GitHub sync configuration file
|
|
15
|
+
*/
|
|
16
|
+
export function getGitHubSyncConfigPath(): string {
|
|
17
|
+
const homeDir = process.env.USERPROFILE || process.env.HOME || "";
|
|
18
|
+
return `${homeDir}/.da-proj-github-config`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if GitHub sync is configured
|
|
23
|
+
*/
|
|
24
|
+
export function isGitHubSyncConfigured(): boolean {
|
|
25
|
+
const configPath = getGitHubSyncConfigPath();
|
|
26
|
+
return existsSync(configPath);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read GitHub sync configuration
|
|
31
|
+
*/
|
|
32
|
+
export async function readGitHubSyncConfig(): Promise<GitHubSyncConfig | null> {
|
|
33
|
+
try {
|
|
34
|
+
const configPath = getGitHubSyncConfigPath();
|
|
35
|
+
|
|
36
|
+
if (!existsSync(configPath)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = await readFile(configPath, "utf-8");
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save GitHub sync configuration
|
|
49
|
+
*/
|
|
50
|
+
export async function saveGitHubSyncConfig(
|
|
51
|
+
config: GitHubSyncConfig
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const configPath = getGitHubSyncConfigPath();
|
|
54
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Delete GitHub sync configuration
|
|
59
|
+
*/
|
|
60
|
+
export async function deleteGitHubSyncConfig(): Promise<void> {
|
|
61
|
+
const configPath = getGitHubSyncConfigPath();
|
|
62
|
+
|
|
63
|
+
if (existsSync(configPath)) {
|
|
64
|
+
const fs = await import("fs/promises");
|
|
65
|
+
await fs.unlink(configPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import type { ExistingSecrets, GitHubRepo } from "../types/index.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// GitHub CLI Utilities
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if GitHub CLI is installed
|
|
14
|
+
*/
|
|
15
|
+
export async function checkGitHubCLI(): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
await execAsync("gh --version");
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get GitHub username from authenticated GitHub CLI
|
|
26
|
+
*/
|
|
27
|
+
export async function getGitHubUsername(): Promise<string> {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execAsync("gh api user --jq .login");
|
|
30
|
+
return stdout.trim();
|
|
31
|
+
} catch (error: any) {
|
|
32
|
+
throw new Error(`Failed to get GitHub username: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// GitHub Secrets (for repository secrets)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a secret exists in the repository
|
|
42
|
+
*/
|
|
43
|
+
export async function checkSecretExists(secretName: string): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execAsync("gh secret list");
|
|
46
|
+
return stdout.includes(secretName);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get information about existing secrets
|
|
54
|
+
*/
|
|
55
|
+
export async function getExistingSecrets(): Promise<ExistingSecrets> {
|
|
56
|
+
const urlExists = await checkSecretExists("PORTFOLIO_API_URL");
|
|
57
|
+
const keyExists = await checkSecretExists("PORTFOLIO_API_KEY");
|
|
58
|
+
return { url: urlExists, key: keyExists };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set a GitHub secret
|
|
63
|
+
*/
|
|
64
|
+
export async function setGitHubSecret(
|
|
65
|
+
name: string,
|
|
66
|
+
value: string
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
await execAsync(`gh secret set ${name} --body "${value}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// GitHub Repository Management
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a private repository using GitHub CLI
|
|
77
|
+
*/
|
|
78
|
+
export async function createPrivateRepo(repoName: string): Promise<string> {
|
|
79
|
+
try {
|
|
80
|
+
// Create private repo with description
|
|
81
|
+
await execAsync(
|
|
82
|
+
`gh repo create ${repoName} --private --description "da-proj configuration sync"`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const username = await getGitHubUsername();
|
|
86
|
+
const repoUrl = `https://github.com/${username}/${repoName}`;
|
|
87
|
+
|
|
88
|
+
log.success(`Created private repository: ${repoName}`);
|
|
89
|
+
return repoUrl;
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
throw new Error(`Failed to create repository: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* List all private repositories for the authenticated user
|
|
97
|
+
*/
|
|
98
|
+
export async function listPrivateRepos(): Promise<GitHubRepo[]> {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await execAsync(
|
|
101
|
+
"gh repo list --json name,url,description,isPrivate --limit 100"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const repos = JSON.parse(stdout);
|
|
105
|
+
|
|
106
|
+
// Filter only private repos
|
|
107
|
+
return repos
|
|
108
|
+
.filter((repo: any) => repo.isPrivate)
|
|
109
|
+
.map((repo: any) => ({
|
|
110
|
+
name: repo.name,
|
|
111
|
+
url: repo.url,
|
|
112
|
+
description: repo.description || undefined,
|
|
113
|
+
private: repo.isPrivate,
|
|
114
|
+
}));
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
throw new Error(`Failed to list repositories: ${error.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a repository exists
|
|
122
|
+
*/
|
|
123
|
+
export async function checkRepoExists(repoUrl: string): Promise<boolean> {
|
|
124
|
+
try {
|
|
125
|
+
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
126
|
+
if (!match) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const [, owner, repo] = match;
|
|
131
|
+
|
|
132
|
+
await execAsync(`gh api repos/${owner}/${repo}`);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// GitHub API File Operations
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Upload a file to GitHub repository using GitHub API
|
|
145
|
+
*/
|
|
146
|
+
export async function uploadFile(
|
|
147
|
+
repoUrl: string,
|
|
148
|
+
filePath: string,
|
|
149
|
+
content: string
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
try {
|
|
152
|
+
// Extract owner and repo from URL
|
|
153
|
+
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
154
|
+
if (!match) {
|
|
155
|
+
throw new Error("Invalid repository URL");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const [, owner, repo] = match;
|
|
159
|
+
|
|
160
|
+
// Encode content to base64
|
|
161
|
+
const base64Content = Buffer.from(content).toString("base64");
|
|
162
|
+
|
|
163
|
+
// Check if file exists to get SHA (required for updates)
|
|
164
|
+
let sha: string | undefined;
|
|
165
|
+
try {
|
|
166
|
+
const { stdout: existingFile } = await execAsync(
|
|
167
|
+
`gh api repos/${owner}/${repo}/contents/${filePath} --jq .sha`
|
|
168
|
+
);
|
|
169
|
+
sha = existingFile.trim();
|
|
170
|
+
} catch {
|
|
171
|
+
// File doesn't exist, that's okay
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Upload using GitHub CLI
|
|
175
|
+
await execAsync(
|
|
176
|
+
`gh api repos/${owner}/${repo}/contents/${filePath} -X PUT -f message="Update ${filePath}" -f content="${base64Content}"${
|
|
177
|
+
sha ? ` -f sha="${sha}"` : ""
|
|
178
|
+
}`
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
log.success(`Uploaded ${filePath} to ${owner}/${repo}`);
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
throw new Error(`Failed to upload file: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Download a file from GitHub repository using GitHub API
|
|
189
|
+
*/
|
|
190
|
+
export async function downloadFile(
|
|
191
|
+
repoUrl: string,
|
|
192
|
+
filePath: string
|
|
193
|
+
): Promise<string> {
|
|
194
|
+
try {
|
|
195
|
+
// Extract owner and repo from URL
|
|
196
|
+
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
197
|
+
if (!match) {
|
|
198
|
+
throw new Error("Invalid repository URL");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const [, owner, repo] = match;
|
|
202
|
+
|
|
203
|
+
// Download file content
|
|
204
|
+
const { stdout } = await execAsync(
|
|
205
|
+
`gh api repos/${owner}/${repo}/contents/${filePath} --jq .content`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Decode from base64
|
|
209
|
+
const base64Content = stdout.trim();
|
|
210
|
+
const content = Buffer.from(base64Content, "base64").toString("utf-8");
|
|
211
|
+
|
|
212
|
+
return content;
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
if (error.message.includes("404")) {
|
|
215
|
+
throw new Error(`File not found: ${filePath}`);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Failed to download file: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the SHA of a file in the repository (needed for updates)
|
|
223
|
+
*/
|
|
224
|
+
export async function getFileSha(
|
|
225
|
+
repoUrl: string,
|
|
226
|
+
filePath: string
|
|
227
|
+
): Promise<string | null> {
|
|
228
|
+
try {
|
|
229
|
+
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
|
230
|
+
if (!match) {
|
|
231
|
+
throw new Error("Invalid repository URL");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const [, owner, repo] = match;
|
|
235
|
+
|
|
236
|
+
const { stdout } = await execAsync(
|
|
237
|
+
`gh api repos/${owner}/${repo}/contents/${filePath} --jq .sha`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return stdout.trim();
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
if (error.message.includes("404")) {
|
|
243
|
+
return null; // File doesn't exist
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`Failed to get file SHA: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Helpers
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generate README.md content for the secrets repository
|
|
255
|
+
*/
|
|
256
|
+
export function generateSecretsRepoReadme(): string {
|
|
257
|
+
return `# da-proj Configuration Sync
|
|
258
|
+
|
|
259
|
+
This repository stores your \`da-proj\` configuration for synchronization across devices.
|
|
260
|
+
|
|
261
|
+
## What's stored here?
|
|
262
|
+
|
|
263
|
+
- \`da-proj-config.json\` - Your portfolio profiles (API keys and URLs)
|
|
264
|
+
|
|
265
|
+
## Security
|
|
266
|
+
|
|
267
|
+
⚠️ **This repository is PRIVATE** - Keep it that way!
|
|
268
|
+
|
|
269
|
+
This file contains sensitive API keys. Never make this repository public.
|
|
270
|
+
|
|
271
|
+
## Usage
|
|
272
|
+
|
|
273
|
+
This repository is managed automatically by \`da-proj\` CLI:
|
|
274
|
+
|
|
275
|
+
\`\`\`bash
|
|
276
|
+
# Upload your local configuration
|
|
277
|
+
bunx da-proj --push
|
|
278
|
+
|
|
279
|
+
# Download configuration to a new device
|
|
280
|
+
bunx da-proj --pull
|
|
281
|
+
|
|
282
|
+
# Check sync status
|
|
283
|
+
bunx da-proj --sync-status
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
## Manual Access
|
|
287
|
+
|
|
288
|
+
If you need to manually edit the configuration:
|
|
289
|
+
|
|
290
|
+
1. Edit \`da-proj-config.json\` in this repository
|
|
291
|
+
2. Run \`bunx da-proj --pull\` on your device to download changes
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
🔐 Generated by [da-proj](https://github.com/your-username/da-proj)
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Colores para la terminal
|
|
2
|
+
const colors = {
|
|
3
|
+
reset: "\x1b[0m",
|
|
4
|
+
bright: "\x1b[1m",
|
|
5
|
+
green: "\x1b[32m",
|
|
6
|
+
blue: "\x1b[34m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const log = {
|
|
13
|
+
success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
14
|
+
error: (msg: string) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
|
15
|
+
info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
|
16
|
+
warn: (msg: string) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
|
17
|
+
title: (msg: string) => console.log(`\n${colors.bright}${colors.cyan}${msg}${colors.reset}\n`),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { colors };
|