czon 0.8.0 → 0.8.2
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/commands/config-github.js +304 -20
- package/dist/process/extractMetadataByAI.js +11 -3
- package/dist/process/scanSourceFiles.js +2 -7
- package/dist/utils/git.js +188 -0
- package/dist/utils/github.js +239 -0
- package/package.json +2 -1
|
@@ -35,44 +35,328 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ConfigGithubCommand = void 0;
|
|
37
37
|
const clipanion_1 = require("clipanion");
|
|
38
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
38
39
|
const fs = __importStar(require("fs/promises"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const writeFile_1 = require("../utils/writeFile");
|
|
42
|
+
const git = __importStar(require("../utils/git"));
|
|
43
|
+
const github = __importStar(require("../utils/github"));
|
|
41
44
|
class ConfigGithubCommand extends clipanion_1.Command {
|
|
42
45
|
async execute() {
|
|
46
|
+
const stdout = this.context.stdout;
|
|
47
|
+
const stderr = this.context.stderr;
|
|
48
|
+
// ========== Step 0: Prerequisites check ==========
|
|
49
|
+
stdout.write('Checking prerequisites...\n');
|
|
50
|
+
// Check gh CLI installation
|
|
51
|
+
if (!github.isGhInstalled()) {
|
|
52
|
+
stderr.write('\nGitHub CLI (gh) is not installed.\n' +
|
|
53
|
+
'Please install it from: https://cli.github.com/\n');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
// Check gh authentication
|
|
57
|
+
if (!github.isGhAuthenticated()) {
|
|
58
|
+
stderr.write('\nGitHub CLI is not authenticated.\n' + 'Please run: gh auth login\n');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
// Check required token scopes
|
|
62
|
+
const requiredScopes = ['repo'];
|
|
63
|
+
const { hasScopes, missingScopes, currentScopes } = github.checkTokenScopes(requiredScopes);
|
|
64
|
+
if (!hasScopes) {
|
|
65
|
+
stderr.write('\nGitHub CLI token is missing required permissions.\n' +
|
|
66
|
+
` Current scopes: ${currentScopes.length > 0 ? currentScopes.join(', ') : '(none detected)'}\n` +
|
|
67
|
+
` Missing scopes: ${missingScopes.join(', ')}\n\n` +
|
|
68
|
+
'Please re-authenticate with the required scopes:\n' +
|
|
69
|
+
' gh auth login --scopes repo\n\n' +
|
|
70
|
+
'Or refresh your existing token:\n' +
|
|
71
|
+
' gh auth refresh --scopes repo\n');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
stdout.write('GitHub CLI detected and authenticated.\n\n');
|
|
75
|
+
// ========== Step 1: Git repository check ==========
|
|
76
|
+
if (!git.isGitRepository()) {
|
|
77
|
+
const shouldInit = await (0, prompts_1.confirm)({
|
|
78
|
+
message: 'Current directory is not a Git repository. Initialize one?',
|
|
79
|
+
default: true,
|
|
80
|
+
});
|
|
81
|
+
if (!shouldInit) {
|
|
82
|
+
stdout.write('Aborted.\n');
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
git.initGitRepository();
|
|
86
|
+
stdout.write('Git repository initialized.\n\n');
|
|
87
|
+
}
|
|
88
|
+
// ========== Step 2: Remote repository configuration ==========
|
|
89
|
+
const remotes = git.getRemotes();
|
|
90
|
+
const githubRemotes = remotes.filter(r => github.isGitHubUrl(r.fetchUrl));
|
|
91
|
+
let selectedRemote;
|
|
92
|
+
if (githubRemotes.length === 0) {
|
|
93
|
+
// No GitHub remote found
|
|
94
|
+
stdout.write('No GitHub remote repository found.\n');
|
|
95
|
+
const action = await (0, prompts_1.select)({
|
|
96
|
+
message: 'What would you like to do?',
|
|
97
|
+
choices: [
|
|
98
|
+
{ name: 'Create a new GitHub repository', value: 'create' },
|
|
99
|
+
{
|
|
100
|
+
name: 'Add an existing GitHub repository URL',
|
|
101
|
+
value: 'add',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
if (action === 'create') {
|
|
106
|
+
// Create new repository
|
|
107
|
+
const repoName = await (0, prompts_1.input)({
|
|
108
|
+
message: 'Repository name:',
|
|
109
|
+
default: path.basename(process.cwd()),
|
|
110
|
+
});
|
|
111
|
+
const isPrivate = await (0, prompts_1.confirm)({
|
|
112
|
+
message: 'Make repository private?',
|
|
113
|
+
default: false,
|
|
114
|
+
});
|
|
115
|
+
stdout.write('Creating GitHub repository...\n');
|
|
116
|
+
const { owner, repo, url } = github.createRepository({
|
|
117
|
+
name: repoName,
|
|
118
|
+
private: isPrivate,
|
|
119
|
+
});
|
|
120
|
+
// Determine remote name
|
|
121
|
+
let remoteName = 'origin';
|
|
122
|
+
if (git.remoteExists('origin')) {
|
|
123
|
+
remoteName = await (0, prompts_1.input)({
|
|
124
|
+
message: 'Remote "origin" already exists. Enter a name for the GitHub remote:',
|
|
125
|
+
default: 'github',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
git.addRemote(remoteName, url);
|
|
129
|
+
selectedRemote = { name: remoteName, owner, repo };
|
|
130
|
+
stdout.write(`Repository created: https://github.com/${owner}/${repo}\n`);
|
|
131
|
+
stdout.write(`Added as remote "${remoteName}".\n\n`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Add existing repository
|
|
135
|
+
const repoUrl = await (0, prompts_1.input)({
|
|
136
|
+
message: 'GitHub repository URL:',
|
|
137
|
+
validate: value => {
|
|
138
|
+
if (!github.isGitHubUrl(value)) {
|
|
139
|
+
return 'Please enter a valid GitHub repository URL';
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const parsed = github.parseGitHubUrl(repoUrl);
|
|
145
|
+
// Determine remote name
|
|
146
|
+
let remoteName = 'origin';
|
|
147
|
+
if (git.remoteExists('origin')) {
|
|
148
|
+
remoteName = await (0, prompts_1.input)({
|
|
149
|
+
message: 'Remote "origin" already exists. Enter a name for the GitHub remote:',
|
|
150
|
+
default: 'github',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
git.addRemote(remoteName, repoUrl);
|
|
154
|
+
selectedRemote = {
|
|
155
|
+
name: remoteName,
|
|
156
|
+
owner: parsed.owner,
|
|
157
|
+
repo: parsed.repo,
|
|
158
|
+
};
|
|
159
|
+
stdout.write(`Remote "${remoteName}" added.\n\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (githubRemotes.length === 1) {
|
|
163
|
+
// Single GitHub remote
|
|
164
|
+
const remote = githubRemotes[0];
|
|
165
|
+
const parsed = github.parseGitHubUrl(remote.fetchUrl);
|
|
166
|
+
selectedRemote = {
|
|
167
|
+
name: remote.name,
|
|
168
|
+
owner: parsed.owner,
|
|
169
|
+
repo: parsed.repo,
|
|
170
|
+
};
|
|
171
|
+
stdout.write(`Using GitHub remote "${remote.name}" (${parsed.owner}/${parsed.repo}).\n\n`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Multiple GitHub remotes - ask user to choose
|
|
175
|
+
const choices = githubRemotes.map(r => {
|
|
176
|
+
const parsed = github.parseGitHubUrl(r.fetchUrl);
|
|
177
|
+
return {
|
|
178
|
+
name: `${r.name} (${parsed.owner}/${parsed.repo})`,
|
|
179
|
+
value: r.name,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const selectedName = await (0, prompts_1.select)({
|
|
183
|
+
message: 'Multiple GitHub remotes found. Which one to configure?',
|
|
184
|
+
choices,
|
|
185
|
+
});
|
|
186
|
+
const remote = githubRemotes.find(r => r.name === selectedName);
|
|
187
|
+
const parsed = github.parseGitHubUrl(remote.fetchUrl);
|
|
188
|
+
selectedRemote = {
|
|
189
|
+
name: remote.name,
|
|
190
|
+
owner: parsed.owner,
|
|
191
|
+
repo: parsed.repo,
|
|
192
|
+
};
|
|
193
|
+
stdout.write('\n');
|
|
194
|
+
}
|
|
195
|
+
stdout.write(`Configuring GitHub Pages for ${selectedRemote.owner}/${selectedRemote.repo}...\n`);
|
|
196
|
+
// ========== Step 3: GitHub Pages configuration ==========
|
|
197
|
+
let pagesInfo;
|
|
43
198
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
199
|
+
pagesInfo = github.getPagesInfo(selectedRemote.owner, selectedRemote.repo);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
stderr.write(`\nFailed to get GitHub Pages info: ${error}\n` +
|
|
203
|
+
'You may need to enable Pages manually in repository settings.\n');
|
|
204
|
+
pagesInfo = { enabled: false };
|
|
205
|
+
}
|
|
206
|
+
if (!pagesInfo.enabled || pagesInfo.buildType !== 'workflow') {
|
|
207
|
+
stdout.write('Enabling GitHub Pages with workflow deployment...\n');
|
|
48
208
|
try {
|
|
49
|
-
|
|
209
|
+
github.enablePages(selectedRemote.owner, selectedRemote.repo);
|
|
210
|
+
stdout.write('GitHub Pages enabled.\n');
|
|
50
211
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
53
|
-
|
|
212
|
+
catch (error) {
|
|
213
|
+
stderr.write(`\nFailed to enable GitHub Pages: ${error}\n` +
|
|
214
|
+
'You may need to enable Pages manually:\n' +
|
|
215
|
+
` https://github.com/${selectedRemote.owner}/${selectedRemote.repo}/settings/pages\n` +
|
|
216
|
+
' Set "Build and deployment" source to "GitHub Actions"\n\n');
|
|
54
217
|
}
|
|
55
|
-
// 读取模板文件
|
|
56
|
-
const content = await fs.readFile(templatePath, 'utf-8');
|
|
57
|
-
// 确保目标目录存在并写入文件
|
|
58
|
-
await (0, writeFile_1.writeFile)(targetPath, content);
|
|
59
|
-
this.context.stdout.write(`GitHub Actions workflow copied to ${targetPath}\n`);
|
|
60
|
-
return 0;
|
|
61
218
|
}
|
|
62
|
-
|
|
63
|
-
|
|
219
|
+
else {
|
|
220
|
+
stdout.write('GitHub Pages is already configured for workflow deployment.\n');
|
|
221
|
+
}
|
|
222
|
+
// CNAME configuration
|
|
223
|
+
const configureCname = await (0, prompts_1.confirm)({
|
|
224
|
+
message: 'Do you want to configure a custom domain (CNAME)?',
|
|
225
|
+
default: false,
|
|
226
|
+
});
|
|
227
|
+
if (configureCname) {
|
|
228
|
+
const cname = await (0, prompts_1.input)({
|
|
229
|
+
message: 'Enter your custom domain (e.g., docs.example.com):',
|
|
230
|
+
validate: value => {
|
|
231
|
+
if (!value || !/^[a-zA-Z0-9][a-zA-Z0-9-_.]*[a-zA-Z0-9]$/.test(value)) {
|
|
232
|
+
return 'Please enter a valid domain name';
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
try {
|
|
238
|
+
github.setPagesCname(selectedRemote.owner, selectedRemote.repo, cname);
|
|
239
|
+
stdout.write(`Custom domain set to: ${cname}\n`);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
stderr.write(`\nFailed to set custom domain: ${error}\n` +
|
|
243
|
+
'You can set it manually in repository settings.\n');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
stdout.write('\n');
|
|
247
|
+
// ========== Step 4: Workflow file management ==========
|
|
248
|
+
const workflowRelativePath = '.github/workflows/pages.yml';
|
|
249
|
+
const workflowPath = path.join(process.cwd(), workflowRelativePath);
|
|
250
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', 'pages.yml');
|
|
251
|
+
let templateContent;
|
|
252
|
+
try {
|
|
253
|
+
templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
stderr.write(`Template file not found: ${templatePath}\n`);
|
|
64
257
|
return 1;
|
|
65
258
|
}
|
|
259
|
+
let workflowNeedsWrite = false;
|
|
260
|
+
let workflowExists = false;
|
|
261
|
+
try {
|
|
262
|
+
const existingContent = await fs.readFile(workflowPath, 'utf-8');
|
|
263
|
+
workflowExists = true;
|
|
264
|
+
if (existingContent !== templateContent) {
|
|
265
|
+
const shouldUpdate = await (0, prompts_1.confirm)({
|
|
266
|
+
message: 'Workflow file exists but differs from template. Update it?',
|
|
267
|
+
default: true,
|
|
268
|
+
});
|
|
269
|
+
if (shouldUpdate) {
|
|
270
|
+
workflowNeedsWrite = true;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
stdout.write('Keeping existing workflow file.\n');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
stdout.write('Workflow file is up to date.\n');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// File doesn't exist
|
|
282
|
+
workflowNeedsWrite = true;
|
|
283
|
+
}
|
|
284
|
+
if (workflowNeedsWrite) {
|
|
285
|
+
await (0, writeFile_1.writeFile)(workflowPath, templateContent);
|
|
286
|
+
stdout.write(workflowExists ? 'Workflow file updated.\n' : 'Workflow file created.\n');
|
|
287
|
+
}
|
|
288
|
+
// ========== Step 5: Commit and push ==========
|
|
289
|
+
const hasWorkflowChanges = git.hasChanges(workflowRelativePath) || git.isUntracked(workflowRelativePath);
|
|
290
|
+
if (hasWorkflowChanges) {
|
|
291
|
+
const shouldCommit = await (0, prompts_1.confirm)({
|
|
292
|
+
message: 'Commit and push the workflow file?',
|
|
293
|
+
default: true,
|
|
294
|
+
});
|
|
295
|
+
if (shouldCommit) {
|
|
296
|
+
try {
|
|
297
|
+
git.stageFiles([workflowRelativePath]);
|
|
298
|
+
git.commit('chore: add GitHub Pages deployment workflow');
|
|
299
|
+
const branch = git.getCurrentBranch() || git.getDefaultBranch();
|
|
300
|
+
stdout.write(`Pushing to ${selectedRemote.name}/${branch}...\n`);
|
|
301
|
+
git.push(selectedRemote.name, branch, { setUpstream: true });
|
|
302
|
+
stdout.write('Changes committed and pushed.\n');
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
stderr.write(`\nFailed to commit/push: ${error}\n`);
|
|
306
|
+
stderr.write('You can manually commit and push the workflow file:\n' +
|
|
307
|
+
` git add ${workflowRelativePath}\n` +
|
|
308
|
+
' git commit -m "chore: add GitHub Pages deployment workflow"\n' +
|
|
309
|
+
` git push -u ${selectedRemote.name} <branch>\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
stdout.write('No changes to commit.\n');
|
|
315
|
+
}
|
|
316
|
+
// ========== Step 6: Show result ==========
|
|
317
|
+
let pagesUrl = null;
|
|
318
|
+
try {
|
|
319
|
+
pagesUrl = github.getPagesUrl(selectedRemote.owner, selectedRemote.repo);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Ignore error
|
|
323
|
+
}
|
|
324
|
+
stdout.write('\n');
|
|
325
|
+
stdout.write('========================================\n');
|
|
326
|
+
stdout.write('GitHub Pages configuration complete!\n');
|
|
327
|
+
stdout.write('========================================\n');
|
|
328
|
+
if (pagesUrl) {
|
|
329
|
+
stdout.write(`\nYour site will be available at:\n ${pagesUrl}\n`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
stdout.write('\nYour site URL will be available after the first deployment.\n' +
|
|
333
|
+
`Check: https://github.com/${selectedRemote.owner}/${selectedRemote.repo}/settings/pages\n`);
|
|
334
|
+
}
|
|
335
|
+
stdout.write('\nThe workflow will automatically deploy on:\n' +
|
|
336
|
+
' - Push to main branch\n' +
|
|
337
|
+
' - Every hour (scheduled)\n' +
|
|
338
|
+
' - Manual trigger from Actions tab\n');
|
|
339
|
+
return 0;
|
|
66
340
|
}
|
|
67
341
|
}
|
|
68
342
|
exports.ConfigGithubCommand = ConfigGithubCommand;
|
|
69
343
|
ConfigGithubCommand.paths = [['config', 'github']];
|
|
70
344
|
ConfigGithubCommand.usage = clipanion_1.Command.Usage({
|
|
71
|
-
description: '
|
|
345
|
+
description: 'Configure GitHub Pages deployment for your CZON site',
|
|
72
346
|
details: `
|
|
73
|
-
This command
|
|
74
|
-
|
|
75
|
-
|
|
347
|
+
This command guides you through setting up GitHub Pages deployment:
|
|
348
|
+
|
|
349
|
+
1. Initialize Git repository (if needed)
|
|
350
|
+
2. Configure GitHub remote repository
|
|
351
|
+
3. Enable GitHub Pages with workflow deployment
|
|
352
|
+
4. Create/update the deployment workflow file
|
|
353
|
+
5. Commit and push changes
|
|
354
|
+
|
|
355
|
+
Prerequisites:
|
|
356
|
+
- GitHub CLI (gh) must be installed and authenticated
|
|
357
|
+
- Install: https://cli.github.com/
|
|
358
|
+
- Login: gh auth login
|
|
359
|
+
|
|
76
360
|
Examples:
|
|
77
361
|
$ czon config github
|
|
78
362
|
`,
|
|
@@ -4,6 +4,7 @@ exports.extractMetadataByAI = extractMetadataByAI;
|
|
|
4
4
|
const promises_1 = require("fs/promises");
|
|
5
5
|
const extractMetadataFromMarkdown_1 = require("../ai/extractMetadataFromMarkdown");
|
|
6
6
|
const metadata_1 = require("../metadata");
|
|
7
|
+
const sha256_1 = require("../utils/sha256");
|
|
7
8
|
/**
|
|
8
9
|
* 运行 AI 元数据提取
|
|
9
10
|
*/
|
|
@@ -16,12 +17,19 @@ async function extractMetadataByAI() {
|
|
|
16
17
|
console.info(`ℹ️ Skipping ${file.path}, not a Markdown file`);
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const content = await (0, promises_1.readFile)(file.path, 'utf-8');
|
|
21
|
+
const hash = (0, sha256_1.sha256)(content);
|
|
22
|
+
// 检查是否已有 metadata 且源文件未变化
|
|
23
|
+
if (file.metadata &&
|
|
24
|
+
file.metadata.slug &&
|
|
25
|
+
file.metadata.short_summary &&
|
|
26
|
+
hash === file.hash) {
|
|
27
|
+
console.info(`ℹ️ Skipping ${file.path}, already has metadata and content unchanged`);
|
|
21
28
|
return;
|
|
22
29
|
}
|
|
23
|
-
const content = await (0, promises_1.readFile)(file.path, 'utf-8');
|
|
24
30
|
file.metadata = await (0, extractMetadataFromMarkdown_1.extractMetadataFromMarkdown)(file.path, content);
|
|
31
|
+
// 记录提取 metadata 时的源文件 hash
|
|
32
|
+
file.hash = hash;
|
|
25
33
|
console.log(`✅ Extracted AI metadata for ${file.path}`, file.metadata.tokens_used);
|
|
26
34
|
}));
|
|
27
35
|
const errors = results.filter(r => r.status === 'rejected');
|
|
@@ -9,7 +9,6 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const findEntries_1 = require("../findEntries");
|
|
10
10
|
const metadata_1 = require("../metadata");
|
|
11
11
|
const paths_1 = require("../paths");
|
|
12
|
-
const sha256_1 = require("../utils/sha256");
|
|
13
12
|
const extractLinksFromMarkdown = (content) => {
|
|
14
13
|
const linkRegex = /\[.*?\]\((.*?)\)/g;
|
|
15
14
|
const links = [];
|
|
@@ -50,21 +49,17 @@ async function scanSourceFiles() {
|
|
|
50
49
|
continue;
|
|
51
50
|
}
|
|
52
51
|
const contentBuffer = await (0, promises_1.readFile)(fullPath);
|
|
53
|
-
const hash = (0, sha256_1.sha256)(contentBuffer);
|
|
54
52
|
paths.add(relativePath);
|
|
55
53
|
let meta = metadata_1.MetaData.files.find(f => f.path === relativePath);
|
|
56
54
|
if (!meta) {
|
|
57
|
-
meta = {
|
|
55
|
+
meta = { path: relativePath, links: [] };
|
|
58
56
|
metadata_1.MetaData.files.push(meta);
|
|
59
57
|
}
|
|
60
|
-
else {
|
|
61
|
-
meta.hash = hash;
|
|
62
|
-
}
|
|
63
58
|
// 处理 Markdown 文件
|
|
64
59
|
if (fullPath.endsWith('.md')) {
|
|
65
60
|
const content = contentBuffer.toString('utf-8');
|
|
66
61
|
const links = extractLinksFromMarkdown(content);
|
|
67
|
-
console.info(` - Found file: ${relativePath}
|
|
62
|
+
console.info(` - Found file: ${relativePath}`);
|
|
68
63
|
console.info(` Links: ${links.join(', ') || 'None'}`);
|
|
69
64
|
meta.links = links;
|
|
70
65
|
for (const link of links) {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isGitRepository = isGitRepository;
|
|
4
|
+
exports.initGitRepository = initGitRepository;
|
|
5
|
+
exports.getRemotes = getRemotes;
|
|
6
|
+
exports.addRemote = addRemote;
|
|
7
|
+
exports.remoteExists = remoteExists;
|
|
8
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
9
|
+
exports.getDefaultBranch = getDefaultBranch;
|
|
10
|
+
exports.stageFiles = stageFiles;
|
|
11
|
+
exports.commit = commit;
|
|
12
|
+
exports.push = push;
|
|
13
|
+
exports.hasChanges = hasChanges;
|
|
14
|
+
exports.isUntracked = isUntracked;
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
/**
|
|
17
|
+
* Execute a git command and return the output
|
|
18
|
+
* @throws Error if the command fails
|
|
19
|
+
*/
|
|
20
|
+
function execGit(args, options) {
|
|
21
|
+
const command = ['git', ...args].join(' ');
|
|
22
|
+
try {
|
|
23
|
+
return (0, child_process_1.execSync)(command, {
|
|
24
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const err = error;
|
|
31
|
+
throw new Error(err.stderr || err.message || 'Git command failed');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if the current directory is a Git repository
|
|
36
|
+
*/
|
|
37
|
+
function isGitRepository() {
|
|
38
|
+
try {
|
|
39
|
+
execGit(['rev-parse', '--is-inside-work-tree']);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Initialize a new Git repository
|
|
48
|
+
*/
|
|
49
|
+
function initGitRepository() {
|
|
50
|
+
execGit(['init']);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all remote repositories
|
|
54
|
+
*/
|
|
55
|
+
function getRemotes() {
|
|
56
|
+
try {
|
|
57
|
+
const output = execGit(['remote', '-v']);
|
|
58
|
+
if (!output)
|
|
59
|
+
return [];
|
|
60
|
+
const lines = output.split('\n');
|
|
61
|
+
const remoteMap = new Map();
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/);
|
|
64
|
+
if (match) {
|
|
65
|
+
const [, name, url, type] = match;
|
|
66
|
+
if (!remoteMap.has(name)) {
|
|
67
|
+
remoteMap.set(name, { name, fetchUrl: '', pushUrl: '' });
|
|
68
|
+
}
|
|
69
|
+
const remote = remoteMap.get(name);
|
|
70
|
+
if (type === 'fetch') {
|
|
71
|
+
remote.fetchUrl = url;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
remote.pushUrl = url;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return Array.from(remoteMap.values());
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Add a remote repository
|
|
86
|
+
*/
|
|
87
|
+
function addRemote(name, url) {
|
|
88
|
+
execGit(['remote', 'add', name, url]);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a remote exists
|
|
92
|
+
*/
|
|
93
|
+
function remoteExists(name) {
|
|
94
|
+
const remotes = getRemotes();
|
|
95
|
+
return remotes.some(r => r.name === name);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get the current branch name
|
|
99
|
+
*/
|
|
100
|
+
function getCurrentBranch() {
|
|
101
|
+
try {
|
|
102
|
+
return execGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get the default branch name (main or master)
|
|
110
|
+
*/
|
|
111
|
+
function getDefaultBranch() {
|
|
112
|
+
try {
|
|
113
|
+
// Try to get the default branch from git config
|
|
114
|
+
const defaultBranch = execGit(['config', '--get', 'init.defaultBranch']);
|
|
115
|
+
if (defaultBranch)
|
|
116
|
+
return defaultBranch;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore error
|
|
120
|
+
}
|
|
121
|
+
// Check if main branch exists
|
|
122
|
+
try {
|
|
123
|
+
execGit(['rev-parse', '--verify', 'main']);
|
|
124
|
+
return 'main';
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore error
|
|
128
|
+
}
|
|
129
|
+
// Check if master branch exists
|
|
130
|
+
try {
|
|
131
|
+
execGit(['rev-parse', '--verify', 'master']);
|
|
132
|
+
return 'master';
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Ignore error
|
|
136
|
+
}
|
|
137
|
+
// Default to main
|
|
138
|
+
return 'main';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Stage files for commit
|
|
142
|
+
*/
|
|
143
|
+
function stageFiles(files) {
|
|
144
|
+
execGit(['add', ...files]);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Commit staged changes
|
|
148
|
+
*/
|
|
149
|
+
function commit(message) {
|
|
150
|
+
execGit(['commit', '-m', message]);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Push to remote repository
|
|
154
|
+
*/
|
|
155
|
+
function push(remote, branch, options) {
|
|
156
|
+
const args = ['push'];
|
|
157
|
+
if (options?.setUpstream) {
|
|
158
|
+
args.push('-u');
|
|
159
|
+
}
|
|
160
|
+
args.push(remote, branch);
|
|
161
|
+
execGit(args);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if a file has uncommitted changes (staged or unstaged)
|
|
165
|
+
*/
|
|
166
|
+
function hasChanges(file) {
|
|
167
|
+
try {
|
|
168
|
+
// Check for both staged and unstaged changes
|
|
169
|
+
const status = execGit(['status', '--porcelain', file]);
|
|
170
|
+
return status.length > 0;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if a file is untracked
|
|
178
|
+
*/
|
|
179
|
+
function isUntracked(file) {
|
|
180
|
+
try {
|
|
181
|
+
const status = execGit(['status', '--porcelain', file]);
|
|
182
|
+
return status.startsWith('??');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isGhInstalled = isGhInstalled;
|
|
4
|
+
exports.isGhAuthenticated = isGhAuthenticated;
|
|
5
|
+
exports.getTokenScopes = getTokenScopes;
|
|
6
|
+
exports.checkTokenScopes = checkTokenScopes;
|
|
7
|
+
exports.parseGitHubUrl = parseGitHubUrl;
|
|
8
|
+
exports.isGitHubUrl = isGitHubUrl;
|
|
9
|
+
exports.createRepository = createRepository;
|
|
10
|
+
exports.getPagesInfo = getPagesInfo;
|
|
11
|
+
exports.enablePages = enablePages;
|
|
12
|
+
exports.setPagesCname = setPagesCname;
|
|
13
|
+
exports.getPagesUrl = getPagesUrl;
|
|
14
|
+
exports.getAuthenticatedUser = getAuthenticatedUser;
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
/**
|
|
17
|
+
* Execute a gh CLI command and return the output
|
|
18
|
+
* Uses spawnSync to avoid shell parsing issues with special characters
|
|
19
|
+
* @throws Error if the command fails
|
|
20
|
+
*/
|
|
21
|
+
function execGh(args) {
|
|
22
|
+
const result = (0, child_process_1.spawnSync)('gh', args, {
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
});
|
|
27
|
+
if (result.error) {
|
|
28
|
+
throw result.error;
|
|
29
|
+
}
|
|
30
|
+
if (result.status !== 0) {
|
|
31
|
+
throw new Error(result.stderr || `gh command failed with exit code ${result.status}`);
|
|
32
|
+
}
|
|
33
|
+
return (result.stdout || '').trim();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if GitHub CLI (gh) is installed
|
|
37
|
+
*/
|
|
38
|
+
function isGhInstalled() {
|
|
39
|
+
try {
|
|
40
|
+
(0, child_process_1.execSync)('gh --version', {
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if GitHub CLI is authenticated
|
|
52
|
+
*/
|
|
53
|
+
function isGhAuthenticated() {
|
|
54
|
+
try {
|
|
55
|
+
execGh(['auth', 'status']);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the token scopes for the current GitHub CLI authentication
|
|
64
|
+
* @returns Array of scope strings, or empty array if unable to determine
|
|
65
|
+
*/
|
|
66
|
+
function getTokenScopes() {
|
|
67
|
+
try {
|
|
68
|
+
const result = (0, child_process_1.spawnSync)('gh', ['auth', 'status'], {
|
|
69
|
+
encoding: 'utf-8',
|
|
70
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
// gh auth status outputs to stderr
|
|
73
|
+
const output = result.stderr || result.stdout || '';
|
|
74
|
+
// Parse token scopes from output
|
|
75
|
+
// Format: "- Token scopes: 'scope1', 'scope2', 'scope3'"
|
|
76
|
+
const scopesMatch = output.match(/Token scopes:\s*(.+)/);
|
|
77
|
+
if (scopesMatch) {
|
|
78
|
+
const scopesStr = scopesMatch[1];
|
|
79
|
+
// Extract scopes from quoted strings like 'repo', 'workflow'
|
|
80
|
+
const scopes = scopesStr.match(/'([^']+)'/g);
|
|
81
|
+
if (scopes) {
|
|
82
|
+
return scopes.map(s => s.replace(/'/g, ''));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if the current authentication has the required scopes
|
|
93
|
+
* @param requiredScopes Array of required scope names
|
|
94
|
+
* @returns Object with hasScopes boolean and missingScopes array
|
|
95
|
+
*/
|
|
96
|
+
function checkTokenScopes(requiredScopes) {
|
|
97
|
+
const currentScopes = getTokenScopes();
|
|
98
|
+
const missingScopes = requiredScopes.filter(required => !currentScopes.includes(required));
|
|
99
|
+
return {
|
|
100
|
+
hasScopes: missingScopes.length === 0,
|
|
101
|
+
missingScopes,
|
|
102
|
+
currentScopes,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse a GitHub URL and extract owner and repo
|
|
107
|
+
* Supports formats:
|
|
108
|
+
* - https://github.com/owner/repo.git
|
|
109
|
+
* - https://github.com/owner/repo
|
|
110
|
+
* - git@github.com:owner/repo.git
|
|
111
|
+
* - git@github.com:owner/repo
|
|
112
|
+
*/
|
|
113
|
+
function parseGitHubUrl(url) {
|
|
114
|
+
// HTTPS format
|
|
115
|
+
const httpsMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
116
|
+
if (httpsMatch) {
|
|
117
|
+
return { owner: httpsMatch[1], repo: httpsMatch[2] };
|
|
118
|
+
}
|
|
119
|
+
// SSH format
|
|
120
|
+
const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
121
|
+
if (sshMatch) {
|
|
122
|
+
return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if a URL is a GitHub repository URL
|
|
128
|
+
*/
|
|
129
|
+
function isGitHubUrl(url) {
|
|
130
|
+
return parseGitHubUrl(url) !== null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a new GitHub repository
|
|
134
|
+
*/
|
|
135
|
+
function createRepository(options) {
|
|
136
|
+
const args = ['repo', 'create', options.name, '--confirm'];
|
|
137
|
+
if (options.private) {
|
|
138
|
+
args.push('--private');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
args.push('--public');
|
|
142
|
+
}
|
|
143
|
+
if (options.description) {
|
|
144
|
+
args.push('--description', options.description);
|
|
145
|
+
}
|
|
146
|
+
const output = execGh(args);
|
|
147
|
+
// Parse the output to get the repo URL
|
|
148
|
+
// Output format: https://github.com/owner/repo
|
|
149
|
+
const urlMatch = output.match(/https:\/\/github\.com\/([^/]+)\/([^\s]+)/);
|
|
150
|
+
if (!urlMatch) {
|
|
151
|
+
throw new Error('Failed to parse repository URL from gh output');
|
|
152
|
+
}
|
|
153
|
+
const owner = urlMatch[1];
|
|
154
|
+
const repo = urlMatch[2];
|
|
155
|
+
return {
|
|
156
|
+
owner,
|
|
157
|
+
repo,
|
|
158
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
159
|
+
isPrivate: options.private ?? false,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get GitHub Pages configuration for a repository
|
|
164
|
+
*/
|
|
165
|
+
function getPagesInfo(owner, repo) {
|
|
166
|
+
try {
|
|
167
|
+
const output = execGh([
|
|
168
|
+
'api',
|
|
169
|
+
`repos/${owner}/${repo}/pages`,
|
|
170
|
+
'--jq',
|
|
171
|
+
'{ url: .html_url, build_type: .build_type, cname: .cname }',
|
|
172
|
+
]);
|
|
173
|
+
const data = JSON.parse(output);
|
|
174
|
+
return {
|
|
175
|
+
enabled: true,
|
|
176
|
+
url: data.url,
|
|
177
|
+
buildType: data.build_type,
|
|
178
|
+
cname: data.cname,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const err = error;
|
|
183
|
+
// 404 means Pages is not enabled
|
|
184
|
+
if (err.message?.includes('404')) {
|
|
185
|
+
return { enabled: false };
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Enable GitHub Pages with workflow deployment
|
|
192
|
+
*/
|
|
193
|
+
function enablePages(owner, repo) {
|
|
194
|
+
try {
|
|
195
|
+
// Try to create/enable Pages with workflow build type
|
|
196
|
+
execGh(['api', `repos/${owner}/${repo}/pages`, '-X', 'POST', '-f', 'build_type=workflow']);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const err = error;
|
|
200
|
+
// If Pages already exists, update it
|
|
201
|
+
if (err.message?.includes('409') || err.message?.includes('already')) {
|
|
202
|
+
execGh(['api', `repos/${owner}/${repo}/pages`, '-X', 'PUT', '-f', 'build_type=workflow']);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Set custom domain (CNAME) for GitHub Pages
|
|
211
|
+
*/
|
|
212
|
+
function setPagesCname(owner, repo, cname) {
|
|
213
|
+
if (cname) {
|
|
214
|
+
execGh(['api', `repos/${owner}/${repo}/pages`, '-X', 'PUT', '-f', `cname=${cname}`]);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Remove CNAME by setting it to empty
|
|
218
|
+
execGh(['api', `repos/${owner}/${repo}/pages`, '-X', 'PUT', '-f', 'cname=']);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the GitHub Pages URL for a repository
|
|
223
|
+
*/
|
|
224
|
+
function getPagesUrl(owner, repo) {
|
|
225
|
+
try {
|
|
226
|
+
const output = execGh(['api', `repos/${owner}/${repo}/pages`, '--jq', '.html_url']);
|
|
227
|
+
return output || null;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get the authenticated user's login name
|
|
235
|
+
*/
|
|
236
|
+
function getAuthenticatedUser() {
|
|
237
|
+
return execGh(['api', 'user', '--jq', '.login']);
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=github.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "czon",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "CZON - AI enhanced Markdown content engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@inquirer/prompts": "^8.2.0",
|
|
53
54
|
"@opencode-ai/sdk": "^1.1.34",
|
|
54
55
|
"@types/react": "^19.2.7",
|
|
55
56
|
"@types/react-dom": "^19.2.3",
|