@alien-protocol/cannon 2.2.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/.env.example +12 -0
- package/README.md +395 -0
- package/bin/cli.js +410 -0
- package/cannon.config.example.json +17 -0
- package/package.json +72 -0
- package/src/auth.js +188 -0
- package/src/cannon.js +401 -0
- package/src/config.js +131 -0
- package/src/github.js +167 -0
- package/src/index.js +5 -0
- package/src/loaders/csv.js +70 -0
- package/src/loaders/docx.js +45 -0
- package/src/loaders/index.js +51 -0
- package/src/loaders/json.js +10 -0
- package/src/loaders/mysql.js +30 -0
- package/src/loaders/pdf.js +62 -0
- package/src/loaders/postgres.js +32 -0
- package/src/loaders/sqlite.js +32 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/cli.js — @alien-protocol/cannon CLI
|
|
4
|
+
*
|
|
5
|
+
* ─── Auth ───────────────────────────────────────────────────────
|
|
6
|
+
* cannon auth login OAuth login via GitHub (no token needed)
|
|
7
|
+
* cannon auth status Show who is currently logged in
|
|
8
|
+
* cannon auth logout Remove saved token
|
|
9
|
+
*
|
|
10
|
+
* ─── Setup ──────────────────────────────────────────────────────
|
|
11
|
+
* cannon init Create a cannon.config.json with defaults
|
|
12
|
+
*
|
|
13
|
+
* ─── Run ────────────────────────────────────────────────────────
|
|
14
|
+
* cannon fire Fire using cannon.config.json
|
|
15
|
+
* cannon fire --dry-run Preview without creating
|
|
16
|
+
* cannon fire --unsafe Skip delays (risky!)
|
|
17
|
+
* cannon fire --source csv --file ./issues.csv (override config)
|
|
18
|
+
*
|
|
19
|
+
* ─── Validate ───────────────────────────────────────────────────
|
|
20
|
+
* cannon validate Validate your config + issues file
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { program, Command } from 'commander';
|
|
24
|
+
import { IssueCannon } from '../src/cannon.js';
|
|
25
|
+
import { login, status, logout } from '../src/auth.js';
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
27
|
+
import { createRequire } from 'module';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
32
|
+
|
|
33
|
+
const c = {
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
bold: '\x1b[1m',
|
|
36
|
+
dim: '\x1b[2m',
|
|
37
|
+
green: '\x1b[32m',
|
|
38
|
+
yellow: '\x1b[33m',
|
|
39
|
+
red: '\x1b[31m',
|
|
40
|
+
cyan: '\x1b[36m',
|
|
41
|
+
blue: '\x1b[34m',
|
|
42
|
+
magenta: '\x1b[35m',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────
|
|
46
|
+
// cannon auth
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────
|
|
48
|
+
const auth = new Command('auth').description(
|
|
49
|
+
'Manage GitHub authentication (login / status / logout)'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
auth
|
|
53
|
+
.command('login')
|
|
54
|
+
.description('Login via GitHub OAuth — no token or .env file needed')
|
|
55
|
+
.action(async () => {
|
|
56
|
+
try {
|
|
57
|
+
await login();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`\n ${c.red}✖${c.reset} ${e.message}\n`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
auth
|
|
65
|
+
.command('status')
|
|
66
|
+
.description('Show who is currently logged in')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
try {
|
|
69
|
+
await status();
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error(`\n ${c.red}✖${c.reset} ${e.message}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
auth
|
|
77
|
+
.command('logout')
|
|
78
|
+
.description('Remove saved GitHub token')
|
|
79
|
+
.action(() => logout());
|
|
80
|
+
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────
|
|
82
|
+
// cannon init — scaffolds a default cannon.config.json
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────
|
|
84
|
+
program
|
|
85
|
+
.command('init')
|
|
86
|
+
.description('Create a cannon.config.json with all default settings')
|
|
87
|
+
.option('--force', 'Overwrite existing cannon.config.json', false)
|
|
88
|
+
.action((opts) => {
|
|
89
|
+
const dest = path.join(process.cwd(), 'cannon.config.json');
|
|
90
|
+
|
|
91
|
+
if (existsSync(dest) && !opts.force) {
|
|
92
|
+
console.log(`\n ${c.yellow}⚠${c.reset} cannon.config.json already exists.`);
|
|
93
|
+
console.log(` Use ${c.bold}cannon init --force${c.reset} to overwrite it.\n`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const config = {
|
|
98
|
+
source: {
|
|
99
|
+
type: 'csv',
|
|
100
|
+
file: './issues.csv',
|
|
101
|
+
},
|
|
102
|
+
mode: {
|
|
103
|
+
safeMode: true,
|
|
104
|
+
dryRun: false,
|
|
105
|
+
resumable: true,
|
|
106
|
+
},
|
|
107
|
+
delay: {
|
|
108
|
+
min: 4,
|
|
109
|
+
max: 8,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
writeFileSync(dest, JSON.stringify(config, null, 2));
|
|
114
|
+
|
|
115
|
+
console.log(`\n ${c.green}✔${c.reset} Created ${c.bold}cannon.config.json${c.reset}\n`);
|
|
116
|
+
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
117
|
+
console.log(
|
|
118
|
+
` 1. Set ${c.bold}source.file${c.reset} to your issues file (or change ${c.bold}source.type${c.reset} for other formats)`
|
|
119
|
+
);
|
|
120
|
+
console.log(` 2. Run ${c.bold}cannon auth login${c.reset}`);
|
|
121
|
+
console.log(` 3. Run ${c.bold}cannon fire --preview${c.reset} to test`);
|
|
122
|
+
console.log(` 4. Run ${c.bold}cannon fire${c.reset} to create issues\n`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────
|
|
126
|
+
// cannon validate — check config + source file before firing
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────
|
|
128
|
+
program
|
|
129
|
+
.command('validate')
|
|
130
|
+
.description('Check your config and issues file for problems')
|
|
131
|
+
.action(async () => {
|
|
132
|
+
console.log(`\n${c.bold}${c.blue}🔍 Validating…${c.reset}\n`);
|
|
133
|
+
let ok = true;
|
|
134
|
+
|
|
135
|
+
// 1. Config file
|
|
136
|
+
const configPath = path.join(process.cwd(), 'cannon.config.json');
|
|
137
|
+
if (!existsSync(configPath)) {
|
|
138
|
+
console.log(` ${c.red}✖${c.reset} cannon.config.json not found`);
|
|
139
|
+
console.log(` Run: ${c.bold}cannon init${c.reset} to create it\n`);
|
|
140
|
+
ok = false;
|
|
141
|
+
} else {
|
|
142
|
+
try {
|
|
143
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
144
|
+
JSON.parse(raw);
|
|
145
|
+
console.log(` ${c.green}✔${c.reset} cannon.config.json ${c.dim}(valid JSON)${c.reset}`);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.log(` ${c.red}✖${c.reset} cannon.config.json has invalid JSON: ${e.message}`);
|
|
148
|
+
ok = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. Token
|
|
153
|
+
try {
|
|
154
|
+
const { loadConfig } = await import('../src/config.js');
|
|
155
|
+
const cfg = loadConfig();
|
|
156
|
+
if (!cfg.github.token) {
|
|
157
|
+
console.log(` ${c.yellow}⚠${c.reset} No GitHub token found`);
|
|
158
|
+
console.log(` Run: ${c.bold}cannon auth login${c.reset} or set GITHUB_TOKEN in .env`);
|
|
159
|
+
ok = false;
|
|
160
|
+
} else {
|
|
161
|
+
console.log(` ${c.green}✔${c.reset} GitHub token ${c.dim}(found)${c.reset}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Source file exists
|
|
165
|
+
if (['csv', 'json', 'pdf', 'docx', 'sqlite'].includes(cfg.source?.type)) {
|
|
166
|
+
const fp = cfg.source?.file;
|
|
167
|
+
if (!fp) {
|
|
168
|
+
console.log(` ${c.red}✖${c.reset} source.file is empty in cannon.config.json`);
|
|
169
|
+
ok = false;
|
|
170
|
+
} else if (!existsSync(path.resolve(fp))) {
|
|
171
|
+
console.log(` ${c.red}✖${c.reset} source.file not found: ${fp}`);
|
|
172
|
+
ok = false;
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` ${c.green}✔${c.reset} source.file ${c.dim}${fp}${c.reset}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Safe mode warning
|
|
179
|
+
if (!cfg.mode?.safeMode) {
|
|
180
|
+
console.log(` ${c.yellow}⚠${c.reset} safeMode is OFF — issues will fire with no delays`);
|
|
181
|
+
console.log(
|
|
182
|
+
` ${c.dim}Set mode.safeMode = true in cannon.config.json for safety${c.reset}`
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(
|
|
186
|
+
` ${c.green}✔${c.reset} safeMode ${c.dim}(on — random delays enabled)${c.reset}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 5. Load + count issues
|
|
191
|
+
if (ok) {
|
|
192
|
+
try {
|
|
193
|
+
const { loadIssues } = await import('../src/loaders/index.js');
|
|
194
|
+
const issues = await loadIssues({
|
|
195
|
+
source: cfg.source.type,
|
|
196
|
+
file: cfg.source.file,
|
|
197
|
+
query: cfg.source.query,
|
|
198
|
+
connectionString: cfg.source.connectionString,
|
|
199
|
+
});
|
|
200
|
+
console.log(
|
|
201
|
+
` ${c.green}✔${c.reset} Issues loaded ${c.dim}(${issues.length} issue(s) found)${c.reset}`
|
|
202
|
+
);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.log(` ${c.red}✖${c.reset} Could not load issues: ${e.message}`);
|
|
205
|
+
ok = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.log(` ${c.red}✖${c.reset} Config error: ${e.message}`);
|
|
210
|
+
ok = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
if (ok) {
|
|
215
|
+
console.log(
|
|
216
|
+
` ${c.green}${c.bold}All checks passed!${c.reset} Run ${c.bold}cannon fire --dry-run${c.reset} to preview.\n`
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(
|
|
220
|
+
` ${c.red}${c.bold}Issues found.${c.reset} Fix the errors above and run ${c.bold}cannon validate${c.reset} again.\n`
|
|
221
|
+
);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────
|
|
227
|
+
// cannon fire — main command
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────
|
|
229
|
+
program
|
|
230
|
+
.command('fire')
|
|
231
|
+
.description('Create GitHub issues from your cannon.config.json')
|
|
232
|
+
|
|
233
|
+
// ── What to fire ──────────────────────────────────────────────
|
|
234
|
+
.option('-s, --source <type>', 'Source type: csv | json | pdf | docx | sqlite | postgres | mysql')
|
|
235
|
+
.option('-f, --file <path>', 'Path to your issues file')
|
|
236
|
+
.option('-q, --query <sql>', 'SQL query (database sources only)')
|
|
237
|
+
|
|
238
|
+
// ── How to fire ───────────────────────────────────────────────
|
|
239
|
+
.option('--preview', 'Dry run — show what would be created, nothing is actually made')
|
|
240
|
+
.option('--unsafe', 'No delays between issues (fast but risky — GitHub may flag as spam)')
|
|
241
|
+
.option('--delay <minutes>', 'Fixed delay between issues in minutes, e.g. --delay 2')
|
|
242
|
+
.option('--fresh', 'Ignore saved progress and start from the beginning')
|
|
243
|
+
|
|
244
|
+
.action(async (opts) => {
|
|
245
|
+
const overrides = {};
|
|
246
|
+
|
|
247
|
+
// --preview = dry run, also skips sleep timer
|
|
248
|
+
if (opts.preview) {
|
|
249
|
+
overrides.dryRun = true;
|
|
250
|
+
overrides.preview = true; // passed through to cannon to skip countdown
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --unsafe = no delays
|
|
254
|
+
if (opts.unsafe) overrides.safeMode = false;
|
|
255
|
+
|
|
256
|
+
// --fresh = ignore resume state
|
|
257
|
+
if (opts.fresh) overrides.resumable = false;
|
|
258
|
+
|
|
259
|
+
// --delay <minutes> = fixed delay override
|
|
260
|
+
if (opts.delay) {
|
|
261
|
+
const mins = parseFloat(opts.delay);
|
|
262
|
+
if (isNaN(mins) || mins < 0) {
|
|
263
|
+
console.error(
|
|
264
|
+
`\n ${c.red}✖${c.reset} --delay must be a number in minutes, e.g. --delay 2\n`
|
|
265
|
+
);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
overrides.delay = { mode: 'fixed', fixedMs: Math.round(mins * 60_000) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const cannon = new IssueCannon(overrides);
|
|
272
|
+
|
|
273
|
+
// Source overrides (CLI flags beat cannon.config.json)
|
|
274
|
+
const sourceOpts = {};
|
|
275
|
+
if (opts.source) sourceOpts.source = opts.source;
|
|
276
|
+
if (opts.file) sourceOpts.file = opts.file;
|
|
277
|
+
if (opts.query) sourceOpts.query = opts.query;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await cannon.fire(sourceOpts);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error(`\n ${c.red}✖${c.reset} ${err.message}\n`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ─────────────────────────────────────────────────────────────────
|
|
288
|
+
// Main program (root)
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────
|
|
290
|
+
program
|
|
291
|
+
.name('cannon')
|
|
292
|
+
.description(
|
|
293
|
+
`${c.bold}${c.magenta}🛸 @alien-protocol/cannon${c.reset} — Bulk-create GitHub issues`
|
|
294
|
+
)
|
|
295
|
+
.version(pkg.version)
|
|
296
|
+
.addCommand(auth);
|
|
297
|
+
|
|
298
|
+
// Show a friendly welcome when run with no arguments
|
|
299
|
+
program.action(() => {
|
|
300
|
+
console.log(`
|
|
301
|
+
${c.bold}${c.magenta}🛸 @alien-protocol/cannon${c.reset} v${pkg.version}
|
|
302
|
+
|
|
303
|
+
${c.bold}First time? 4 steps:${c.reset}
|
|
304
|
+
|
|
305
|
+
${c.cyan}1.${c.reset} ${c.bold}cannon init${c.reset} ${c.dim}Create your config file${c.reset}
|
|
306
|
+
${c.cyan}2.${c.reset} ${c.bold}cannon auth login${c.reset} ${c.dim}Login with GitHub${c.reset}
|
|
307
|
+
${c.cyan}3.${c.reset} ${c.bold}cannon fire --preview${c.reset} ${c.dim}Preview what will be created${c.reset}
|
|
308
|
+
${c.cyan}4.${c.reset} ${c.bold}cannon fire${c.reset} ${c.dim}Create your issues!${c.reset}
|
|
309
|
+
|
|
310
|
+
${c.bold}Common flags:${c.reset}
|
|
311
|
+
|
|
312
|
+
${c.bold}--preview${c.reset} ${c.dim}Dry run — nothing created${c.reset}
|
|
313
|
+
${c.bold}--unsafe${c.reset} ${c.dim}No delays (fast, risky)${c.reset}
|
|
314
|
+
${c.bold}--delay 2${c.reset} ${c.dim}Wait 2 minutes between issues${c.reset}
|
|
315
|
+
${c.bold}--fresh${c.reset} ${c.dim}Ignore saved progress, start over${c.reset}
|
|
316
|
+
|
|
317
|
+
${c.bold}Use a different file without editing config:${c.reset}
|
|
318
|
+
|
|
319
|
+
${c.bold}cannon fire -s csv -f ./issues.csv${c.reset}
|
|
320
|
+
${c.bold}cannon fire -s json -f ./issues.json --preview${c.reset}
|
|
321
|
+
|
|
322
|
+
${c.dim}Help:${c.reset} cannon fire --help | ${c.dim}Docs:${c.reset} https://github.com/Alien-Protocol/Cannon
|
|
323
|
+
`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
program.parse(process.argv);
|
|
327
|
+
|
|
328
|
+
// dead code kept only so the module doesn't error if imported elsewhere
|
|
329
|
+
function getDefaultConfig() {
|
|
330
|
+
return {
|
|
331
|
+
_readme:
|
|
332
|
+
"Cannon config. Edit this file then run 'cannon fire'. Docs: https://github.com/Alien-Protocol/Cannon",
|
|
333
|
+
|
|
334
|
+
github: {
|
|
335
|
+
token: '',
|
|
336
|
+
_tokenNote:
|
|
337
|
+
"LEAVE BLANK — use 'cannon auth login' (OAuth) or GITHUB_TOKEN env var. Never put your real token here.",
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
source: {
|
|
341
|
+
type: 'csv',
|
|
342
|
+
_typeNote: 'Options: csv | json | pdf | docx | postgres | mysql | sqlite',
|
|
343
|
+
file: './issues.csv',
|
|
344
|
+
_fileNote: 'Path to your issues file (csv, json, pdf, docx, sqlite).',
|
|
345
|
+
query: '',
|
|
346
|
+
_queryNote: 'SQL query string (postgres, mysql, sqlite).',
|
|
347
|
+
connectionString: '',
|
|
348
|
+
_connectionStringNote: 'DB URL. Use env vars like ${POSTGRES_URL} — never hardcode secrets.',
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
mode: {
|
|
352
|
+
dryRun: false,
|
|
353
|
+
_dryRunNote: 'true = preview only, nothing is created. Useful for testing.',
|
|
354
|
+
safeMode: true,
|
|
355
|
+
_safeModeNote:
|
|
356
|
+
'true = random delays between issues (RECOMMENDED). false = fire immediately (risky).',
|
|
357
|
+
resumable: true,
|
|
358
|
+
_resumableNote: 'true = saves progress so you can stop and restart without duplicates.',
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
delay: {
|
|
362
|
+
_note: 'Only applies when mode.safeMode = true',
|
|
363
|
+
mode: 'random',
|
|
364
|
+
_modeNote: 'random = between minMs and maxMs | fixed = always fixedMs',
|
|
365
|
+
minMs: 240000,
|
|
366
|
+
_minMsNote: 'Minimum delay ms. Default 240000 (4 min). Do not go below 60000.',
|
|
367
|
+
maxMs: 480000,
|
|
368
|
+
_maxMsNote: 'Maximum delay ms. Default 480000 (8 min).',
|
|
369
|
+
fixedMs: 300000,
|
|
370
|
+
_fixedMsNote: "Fixed delay ms when mode = 'fixed'. Default 300000 (5 min).",
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
labels: {
|
|
374
|
+
autoCreate: false,
|
|
375
|
+
_autoCreateNote: 'true = auto-create any missing labels in GitHub.',
|
|
376
|
+
colorMap: {},
|
|
377
|
+
_colorMapNote: "Map label names to hex colors. e.g. { 'bug': 'ee0701', 'feature': '0075ca' }",
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
output: {
|
|
381
|
+
logFile: '',
|
|
382
|
+
_logFileNote: "Optional path for a JSON log of results. e.g. './cannon-log.json'",
|
|
383
|
+
showTable: true,
|
|
384
|
+
_showTableNote: 'true = show a summary table after completion.',
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
_examples: {
|
|
388
|
+
_note: "Copy one block into 'source' above to change your data source.",
|
|
389
|
+
csv: { type: 'csv', file: './issues.csv' },
|
|
390
|
+
json: { type: 'json', file: './issues.json' },
|
|
391
|
+
pdf: { type: 'pdf', file: './issues.pdf' },
|
|
392
|
+
docx: { type: 'docx', file: './issues.docx' },
|
|
393
|
+
postgres: {
|
|
394
|
+
type: 'postgres',
|
|
395
|
+
connectionString: '${POSTGRES_URL}',
|
|
396
|
+
query: 'SELECT repo, title, body, labels, milestone FROM backlog WHERE exported = false',
|
|
397
|
+
},
|
|
398
|
+
mysql: {
|
|
399
|
+
type: 'mysql',
|
|
400
|
+
connectionString: '${MYSQL_URL}',
|
|
401
|
+
query: "SELECT repo, title, body, labels, milestone FROM issues WHERE status = 'pending'",
|
|
402
|
+
},
|
|
403
|
+
sqlite: {
|
|
404
|
+
type: 'sqlite',
|
|
405
|
+
file: './backlog.db',
|
|
406
|
+
query: 'SELECT repo, title, body, labels, milestone FROM issues',
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alien-protocol/cannon",
|
|
3
|
+
"version": "2.2.2",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Bulk-create GitHub issues from CSV, PDF, DOCX, JSON or any database — with OAuth login — by Alien Protocol",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"cannon": "bin/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"src/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"cannon.config.example.json",
|
|
21
|
+
".env.example"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node bin/cli.js",
|
|
25
|
+
"format": "prettier --write .",
|
|
26
|
+
"format:check": "prettier --check ."
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"github",
|
|
30
|
+
"issues",
|
|
31
|
+
"bulk",
|
|
32
|
+
"automation",
|
|
33
|
+
"csv",
|
|
34
|
+
"oauth",
|
|
35
|
+
"cli",
|
|
36
|
+
"alien-protocol"
|
|
37
|
+
],
|
|
38
|
+
"author": "Alien Protocol <https://github.com/Alien-Protocol>",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/Alien-Protocol/Cannon.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/Alien-Protocol/Cannon#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/Alien-Protocol/Cannon/issues"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"chalk": "^5.3.0",
|
|
50
|
+
"commander": "^12.0.0",
|
|
51
|
+
"conf": "^13.0.0",
|
|
52
|
+
"csv-parse": "^5.5.5",
|
|
53
|
+
"dotenv": "^16.4.5",
|
|
54
|
+
"mammoth": "^1.7.2",
|
|
55
|
+
"mysql2": "^3.9.7",
|
|
56
|
+
"ora": "^8.0.1",
|
|
57
|
+
"pdf-parse": "^1.1.1",
|
|
58
|
+
"pg": "^8.11.5",
|
|
59
|
+
"sql.js": "^1.12.0"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
66
|
+
"@semantic-release/github": "^12.0.6",
|
|
67
|
+
"@semantic-release/npm": "^13.1.5",
|
|
68
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
70
|
+
"semantic-release": "^25.0.3"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────
|
|
6
|
+
// GitHub OAuth App client_id
|
|
7
|
+
// This is a PUBLIC value — safe to ship in source.
|
|
8
|
+
// Create your own app at: github.com/settings/developers → OAuth Apps → New
|
|
9
|
+
// Homepage URL: https://github.com/Alien-Protocol/Cannon
|
|
10
|
+
// Callback URL: http://localhost (not used — Device Flow doesn't redirect)
|
|
11
|
+
// Device Flow: ✅ Enable Device Flow
|
|
12
|
+
// Then set CANNON_CLIENT_ID env var or replace the fallback string below.
|
|
13
|
+
// ─────────────────────────────────────────────
|
|
14
|
+
const CLIENT_ID = 'Ov23li9tDhpIemxGcKs6';
|
|
15
|
+
const SCOPES = 'repo';
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cannon');
|
|
18
|
+
const CREDS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
19
|
+
|
|
20
|
+
const c = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bold: '\x1b[1m',
|
|
23
|
+
dim: '\x1b[2m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
blue: '\x1b[34m',
|
|
29
|
+
magenta: '\x1b[35m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function login() {
|
|
33
|
+
console.log(`\n${c.bold}${c.magenta}🛸 Cannon — GitHub Login${c.reset}\n`);
|
|
34
|
+
|
|
35
|
+
if (CLIENT_ID === 'YOUR_GITHUB_OAUTH_CLIENT_ID') {
|
|
36
|
+
console.log(` ${c.red}✖${c.reset} OAuth App not configured.\n`);
|
|
37
|
+
console.log(` ${c.bold}Option A — Set env var:${c.reset}`);
|
|
38
|
+
console.log(` export CANNON_CLIENT_ID=your_client_id\n`);
|
|
39
|
+
console.log(` ${c.bold}Option B — Create a GitHub OAuth App:${c.reset}`);
|
|
40
|
+
console.log(` https://github.com/settings/developers → OAuth Apps → New\n`);
|
|
41
|
+
console.log(` ${c.bold}Option C — Use a token directly (no OAuth needed):${c.reset}`);
|
|
42
|
+
console.log(` echo "GITHUB_TOKEN=ghp_xxx" > .env\n`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Request device + user codes
|
|
47
|
+
const deviceRes = await fetch('https://github.com/login/device/code', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
50
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPES }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!deviceRes.ok) {
|
|
54
|
+
throw new Error(`GitHub device flow failed: ${deviceRes.status}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { device_code, user_code, verification_uri, expires_in, interval } = await deviceRes.json();
|
|
58
|
+
|
|
59
|
+
// 2. Show code to user
|
|
60
|
+
console.log(` ${c.bold}Step 1${c.reset} — Open this URL:\n`);
|
|
61
|
+
console.log(` ${c.blue}${c.bold}${verification_uri}${c.reset}\n`);
|
|
62
|
+
console.log(` ${c.bold}Step 2${c.reset} — Enter this code:\n`);
|
|
63
|
+
console.log(` ${c.cyan}${c.bold} ${user_code} ${c.reset}\n`);
|
|
64
|
+
console.log(` ${c.dim}Expires in ${Math.floor(expires_in / 60)} minutes${c.reset}\n`);
|
|
65
|
+
|
|
66
|
+
// Try auto-open browser
|
|
67
|
+
_openBrowser(verification_uri);
|
|
68
|
+
|
|
69
|
+
// 3. Poll for token
|
|
70
|
+
process.stdout.write(` ${c.dim}⏳ Waiting for authorization…${c.reset}`);
|
|
71
|
+
const token = await _poll(device_code, interval || 5);
|
|
72
|
+
process.stdout.write('\r' + ' '.repeat(55) + '\r');
|
|
73
|
+
|
|
74
|
+
// 4. Get GitHub username
|
|
75
|
+
const user = await _getUser(token);
|
|
76
|
+
|
|
77
|
+
// 5. Save securely
|
|
78
|
+
_saveToken(token, user.login);
|
|
79
|
+
|
|
80
|
+
console.log(` ${c.green}✔${c.reset} Logged in as ${c.bold}${user.login}${c.reset}`);
|
|
81
|
+
console.log(` ${c.green}✔${c.reset} Token saved → ${c.dim}${CREDS_FILE}${c.reset}\n`);
|
|
82
|
+
console.log(
|
|
83
|
+
` ${c.dim}No need to set GITHUB_TOKEN — cannon will use this automatically.${c.reset}\n`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return { token, username: user.login };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function status() {
|
|
90
|
+
const creds = _loadToken();
|
|
91
|
+
|
|
92
|
+
if (!creds) {
|
|
93
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Not logged in.\n`);
|
|
94
|
+
console.log(` Run: ${c.bold}cannon auth login${c.reset} — OAuth (recommended)\n`);
|
|
95
|
+
console.log(` Or: echo "GITHUB_TOKEN=ghp_xxx" > .env\n`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const user = await _getUser(creds.token);
|
|
101
|
+
console.log(`\n ${c.green}✔${c.reset} Logged in as ${c.bold}${user.login}${c.reset}`);
|
|
102
|
+
console.log(` ${c.dim}Credentials: ${CREDS_FILE}${c.reset}`);
|
|
103
|
+
console.log(` ${c.dim}Saved at: ${creds.savedAt}${c.reset}\n`);
|
|
104
|
+
return user;
|
|
105
|
+
} catch {
|
|
106
|
+
console.log(`\n ${c.red}✖${c.reset} Token is invalid or expired.`);
|
|
107
|
+
console.log(` Run: ${c.bold}cannon auth login${c.reset}\n`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function logout() {
|
|
113
|
+
if (fs.existsSync(CREDS_FILE)) {
|
|
114
|
+
fs.unlinkSync(CREDS_FILE);
|
|
115
|
+
console.log(`\n ${c.green}✔${c.reset} Logged out.\n`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getSavedToken() {
|
|
122
|
+
return _loadToken()?.token ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function _poll(deviceCode, intervalSec) {
|
|
126
|
+
const wait = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
127
|
+
while (true) {
|
|
128
|
+
await wait(intervalSec);
|
|
129
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
client_id: CLIENT_ID,
|
|
134
|
+
device_code: deviceCode,
|
|
135
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
if (data.access_token) return data.access_token;
|
|
140
|
+
if (data.error === 'authorization_pending') continue;
|
|
141
|
+
if (data.error === 'slow_down') {
|
|
142
|
+
await wait(5);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (data.error === 'expired_token') throw new Error('Code expired. Run: cannon auth login');
|
|
146
|
+
if (data.error === 'access_denied') throw new Error('Authorization denied.');
|
|
147
|
+
throw new Error(`OAuth error: ${data.error}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function _getUser(token) {
|
|
152
|
+
const res = await fetch('https://api.github.com/user', {
|
|
153
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
|
156
|
+
return res.json();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _saveToken(token, username) {
|
|
160
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
CREDS_FILE,
|
|
163
|
+
JSON.stringify({ token, username, savedAt: new Date().toISOString() }, null, 2),
|
|
164
|
+
{ mode: 0o600 }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _loadToken() {
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(CREDS_FILE)) return JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8'));
|
|
171
|
+
} catch {}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _openBrowser(url) {
|
|
176
|
+
try {
|
|
177
|
+
const { execSync } = require('child_process');
|
|
178
|
+
const cmd =
|
|
179
|
+
process.platform === 'darwin'
|
|
180
|
+
? `open "${url}"`
|
|
181
|
+
: process.platform === 'win32'
|
|
182
|
+
? `start "" "${url}"`
|
|
183
|
+
: `xdg-open "${url}"`;
|
|
184
|
+
execSync(cmd);
|
|
185
|
+
} catch {
|
|
186
|
+
/* user opens manually */
|
|
187
|
+
}
|
|
188
|
+
}
|