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.
@@ -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 "![Synced to Portfolio](https://img.shields.io/badge/portfolio-synced-success)" >> $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 };