@clunkyard/git 0.0.0-stage → 2.0.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/LICENSE +21 -0
- package/README.md +43 -2
- package/client.cjs +59 -0
- package/git-remote-clunkyard +121 -0
- package/login.cjs +31 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clunkyard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Private Clunkyard Git
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Requires Node 20 or later and Git. Install the reviewed release with `npm install --global @clunkyard/git@2.0.1`;
|
|
4
|
+
no language toolchain is added. A downloaded release tarball can also be installed
|
|
5
|
+
with `npm install --global ./clunkyard-git-2.0.1.tgz`. On Linux/macOS, add
|
|
6
|
+
`--prefix "$HOME/.local"` and put `$HOME/.local/bin` on PATH for a user-owned install.
|
|
7
|
+
|
|
8
|
+
This package provides command-line tools; it has no supported JavaScript module API.
|
|
9
|
+
|
|
10
|
+
Create an **agent access key** on your Clunkyard Account page, then run:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
clunkyard-git login
|
|
14
|
+
git clone clunkyard::https://clunkyard.com/bots/BOT_ID my-robot
|
|
15
|
+
cd my-robot
|
|
16
|
+
# Edit your robot, then use ordinary Git.
|
|
17
|
+
git add bot.vane
|
|
18
|
+
git commit -m "Improve target selection"
|
|
19
|
+
git push origin main
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The key is entered without echo and stored in your user configuration directory.
|
|
23
|
+
Keep that directory outside your repository, including when overriding
|
|
24
|
+
`XDG_CONFIG_HOME` or `APPDATA`. An agent key acts for your account; revoke it from the
|
|
25
|
+
Account page when no longer needed. It never goes in the remote URL or project.
|
|
26
|
+
For a secret-manager pipeline, `clunkyard-git login --token-stdin` also works.
|
|
27
|
+
|
|
28
|
+
The initial clone starts ordinary Git history at your current saved project.
|
|
29
|
+
Older application revisions remain in Clunkyard's revision history. Subsequent
|
|
30
|
+
browser and WebDAV saves become commits on the same `main` branch. Use `git
|
|
31
|
+
fetch`, inspect the change and merge before pushing stale work. Only `main` is
|
|
32
|
+
supported; force pushes, branch creation, tags, submodules and symlinks are not.
|
|
33
|
+
Push at most 32 new commits at once, within the project's file and source limits.
|
|
34
|
+
|
|
35
|
+
Binary files and executable bits are retained. Git does not represent empty
|
|
36
|
+
directories or arbitrary Unix permission bits; existing empty directories and
|
|
37
|
+
non-executable permissions remain application metadata. Historical project
|
|
38
|
+
`.git` files are never checked out or executed and remain private application
|
|
39
|
+
history. A push cannot replace them.
|
|
40
|
+
|
|
41
|
+
Successful pushes publish an immutable source revision and request its normal
|
|
42
|
+
hosted build. Compilation may fail independently; inspect the build in My bots.
|
|
43
|
+
An interrupted push retains an operation receipt inside your local Git directory;
|
|
44
|
+
retry `git push` to reconcile it without duplicating a saved revision.
|
package/client.cjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function credentialPath() {
|
|
8
|
+
const root = process.platform === 'win32' ? process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming') : process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
9
|
+
if (!path.isAbsolute(root)) throw Error('The Clunkyard configuration directory must be absolute.');
|
|
10
|
+
return path.join(root, 'clunkyard', 'credentials.json');
|
|
11
|
+
}
|
|
12
|
+
function origin(value) {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) throw Error('Use an HTTPS Clunkyard origin, without a path or credentials.');
|
|
15
|
+
return url.origin;
|
|
16
|
+
}
|
|
17
|
+
function readCredentials() {
|
|
18
|
+
const file = credentialPath();
|
|
19
|
+
let stat; try { stat = fs.lstatSync(file); } catch (error) { if (error.code === 'ENOENT') return {}; throw Error('Cannot read Clunkyard credentials.'); }
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536 || process.platform !== 'win32' && (stat.mode & 0o077 || stat.uid !== process.getuid())) throw Error('Clunkyard credentials must be a private, owner-only file.');
|
|
21
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw Error('Invalid Clunkyard credentials.');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function saveCredential(site, key) {
|
|
26
|
+
site = origin(site);
|
|
27
|
+
if (typeof key !== 'string' || !key || /\s/.test(key) || key.length > 1024) throw Error('Invalid Clunkyard access key.');
|
|
28
|
+
const file = credentialPath(), directory = path.dirname(file), credentials = readCredentials();
|
|
29
|
+
fs.mkdirSync(directory, { recursive:true, mode:0o700 });
|
|
30
|
+
const stat = fs.lstatSync(directory);
|
|
31
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== 'win32' && (stat.mode & 0o077 || stat.uid !== process.getuid())) throw Error('Clunkyard credentials need a private configuration directory.');
|
|
32
|
+
credentials[site] = key;
|
|
33
|
+
const temporary = path.join(directory, `.credentials-${require('node:crypto').randomUUID()}`);
|
|
34
|
+
try { fs.writeFileSync(temporary, JSON.stringify(credentials) + '\n', { mode:0o600, flag:'wx' }); fs.renameSync(temporary, file); }
|
|
35
|
+
finally { fs.rmSync(temporary, { force:true }); }
|
|
36
|
+
return file;
|
|
37
|
+
}
|
|
38
|
+
function keyFor(site) {
|
|
39
|
+
// An environment key is scoped too: a hostile Git remote must not receive
|
|
40
|
+
// the player's production credential merely because it is in the shell.
|
|
41
|
+
if (process.env.CLUNKYARD_AGENT_KEY && site === origin(process.env.CLUNKYARD_ORIGIN || 'https://clunkyard.com')) return process.env.CLUNKYARD_AGENT_KEY;
|
|
42
|
+
const key = readCredentials()[site];
|
|
43
|
+
if (!key) throw Error(`Sign in first with clunkyard-git login --origin ${site}`);
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
async function readJson(response, limit = 40 * 1024 * 1024) {
|
|
47
|
+
const reader = response.body?.getReader(), chunks = []; let size = 0;
|
|
48
|
+
if (!reader) throw Error('Clunkyard returned an empty response.');
|
|
49
|
+
for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > limit) { await reader.cancel(); throw Error('Clunkyard response exceeds the client limit.'); } chunks.push(Buffer.from(value)); }
|
|
50
|
+
let result; try { result = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw Error('Clunkyard returned an invalid response.'); }
|
|
51
|
+
if (!response.ok) throw Object.assign(Error(typeof result.error === 'string' ? result.error.slice(0, 240) : `Clunkyard request failed (${response.status}).`), { status:response.status });
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
async function request(site, route, { method = 'GET', body, operationId, key = keyFor(site) } = {}) {
|
|
55
|
+
const response = await fetch(site + route, { method, redirect:'error', signal:AbortSignal.timeout(45000), headers:{ Authorization:`Bearer ${key}`, Accept:'application/json', ...(body === undefined ? {} : { 'Content-Type':'application/json' }), ...(operationId ? { 'X-Clunkyard-Request':operationId } : {}) }, ...(body === undefined ? {} : { body:JSON.stringify(body) }) });
|
|
56
|
+
return readJson(response);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { credentialPath, origin, readCredentials, saveCredential, keyFor, request };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const readline = require('node:readline');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
const { origin, request } = require('./client.cjs');
|
|
10
|
+
|
|
11
|
+
const OID = /^[0-9a-f]{40}$/;
|
|
12
|
+
function git(args, options = {}) { return execFileSync('git', ['-c', 'core.hooksPath=', ...args], { stdio:['pipe', 'pipe', 'pipe'], maxBuffer:40 * 1024 * 1024, ...options }); }
|
|
13
|
+
function objectId(type, data) { return crypto.createHash('sha1').update(`${type} ${data.length}\0`).update(data).digest('hex'); }
|
|
14
|
+
function objectRefs(value) {
|
|
15
|
+
if (value.type === 'blob') return [];
|
|
16
|
+
if (value.type === 'commit') { const end = value.data.indexOf('\n\n'); return value.data.subarray(0, end).toString('utf8').split('\n').filter(line => /^(tree|parent) /.test(line)).map(line => line.split(' ')[1]); }
|
|
17
|
+
const ids = []; let offset = 0;
|
|
18
|
+
while (offset < value.data.length) { const nul = value.data.indexOf(0, offset); if (nul < 0 || nul + 21 > value.data.length) throw Error('Invalid Git tree received.'); ids.push(value.data.subarray(nul + 1, nul + 21).toString('hex')); offset = nul + 21; }
|
|
19
|
+
return ids;
|
|
20
|
+
}
|
|
21
|
+
const raw = process.argv[3];
|
|
22
|
+
let url;
|
|
23
|
+
try { url = new URL(String(raw).replace(/^clunkyard::/, '')); } catch { throw Error('Use clunkyard::https://clunkyard.com/bots/BOT_ID'); }
|
|
24
|
+
if (url.username || url.password || url.search || url.hash || !/^\/bots\/[0-9a-f-]{36}$/.test(url.pathname)) throw Error('Invalid private bot remote.');
|
|
25
|
+
const site = origin(url.origin), botId = url.pathname.split('/').at(-1), route = `/api/bots/${botId}/git`;
|
|
26
|
+
const ask = (suffix = '', options) => request(site, route + suffix, options);
|
|
27
|
+
let advertised;
|
|
28
|
+
const say = text => process.stdout.write(text + '\n');
|
|
29
|
+
const cleanError = error => String(error.message || 'Private Git operation failed.').replace(/[\r\n]/g, ' ').slice(0, 240);
|
|
30
|
+
|
|
31
|
+
async function fetchObjects(ids) {
|
|
32
|
+
const visited = new Set(), queue = [...ids];
|
|
33
|
+
while (queue.length) {
|
|
34
|
+
const batch = [...new Set(queue.splice(0, 128))].filter(id => !visited.has(id));
|
|
35
|
+
if (!batch.length) continue;
|
|
36
|
+
for (const id of batch) if (!OID.test(id)) throw Error('Invalid Git object requested.');
|
|
37
|
+
const answer = await ask('/objects', { method:'POST', body:{ oids:batch } });
|
|
38
|
+
for (const wire of answer.objects || []) {
|
|
39
|
+
if (!batch.includes(wire.oid) || !['blob', 'tree', 'commit'].includes(wire.type) || typeof wire.data !== 'string') throw Error('Invalid Git object received.');
|
|
40
|
+
const data = Buffer.from(wire.data, 'base64');
|
|
41
|
+
if (data.toString('base64') !== wire.data || objectId(wire.type, data) !== wire.oid) throw Error('Git object failed verification.');
|
|
42
|
+
const actual = git(['hash-object', '-w', '-t', wire.type, '--stdin'], { input:data }).toString().trim();
|
|
43
|
+
if (actual !== wire.oid) throw Error('Git rejected an object.');
|
|
44
|
+
visited.add(wire.oid); queue.push(...objectRefs({ ...wire, data }));
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(answer.objects) || !answer.objects.length) throw Error('Missing Git objects in response.');
|
|
47
|
+
queue.push(...(answer.remaining || []));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function localObjects(head, base, maxCommits) {
|
|
51
|
+
const commits = git(['rev-list', `--max-count=${maxCommits + 1}`, head, `^${base}`]).toString().trim().split('\n').filter(Boolean);
|
|
52
|
+
if (commits.length > maxCommits) throw Error(`Push at most ${maxCommits} commits at a time.`);
|
|
53
|
+
const ids = git(['rev-list', '--objects', head, `^${base}`]).toString().split('\n').filter(Boolean).map(line => line.slice(0, 40));
|
|
54
|
+
if (ids.some(id => !OID.test(id))) throw Error('Cannot list the local Git objects.');
|
|
55
|
+
const data = git(['cat-file', '--batch'], { input:ids.join('\n') + '\n' }), objects = []; let offset = 0;
|
|
56
|
+
for (const expected of ids) {
|
|
57
|
+
const newline = data.indexOf(10, offset), line = data.subarray(offset, newline).toString('ascii'), parsed = line.match(/^([0-9a-f]{40}) (blob|tree|commit) ([0-9]+)$/);
|
|
58
|
+
if (!parsed || parsed[1] !== expected) throw Error('Cannot read a local Git object.');
|
|
59
|
+
const size = Number(parsed[3]), bytes = data.subarray(newline + 1, newline + 1 + size);
|
|
60
|
+
if (bytes.length !== size || data[newline + 1 + size] !== 10) throw Error('Invalid local Git object size.');
|
|
61
|
+
objects.push({ oid:expected, type:parsed[2], data:bytes.toString('base64') }); offset = newline + size + 2;
|
|
62
|
+
}
|
|
63
|
+
return objects;
|
|
64
|
+
}
|
|
65
|
+
function pushReceipt(head, base) {
|
|
66
|
+
const directory = git(['rev-parse', '--absolute-git-dir']).toString().trim(), file = path.join(directory, 'clunkyard-push.json');
|
|
67
|
+
let receipt; try { const stat = fs.lstatSync(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) throw Error('Invalid private Git push receipt.'); receipt = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
68
|
+
if (!receipt || receipt.site !== site || receipt.botId !== botId || receipt.head !== head || receipt.base !== base) {
|
|
69
|
+
receipt = { site, botId, head, base, operationId:crypto.randomUUID() };
|
|
70
|
+
fs.writeFileSync(file, JSON.stringify(receipt) + '\n', { mode:0o600, flag:'w' });
|
|
71
|
+
}
|
|
72
|
+
return { receipt, clear:() => fs.rmSync(file, { force:true }) };
|
|
73
|
+
}
|
|
74
|
+
async function waitForPush(value) {
|
|
75
|
+
for (let attempt = 0; value.state === 'pending' && attempt < 10; attempt++) {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
77
|
+
value = await ask(`/push/${value.operationId}`, { method:'POST' });
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
async function push(spec) {
|
|
82
|
+
let target = spec.split(':').at(-1);
|
|
83
|
+
try {
|
|
84
|
+
const match = spec.match(/^([^:]+):(refs\/heads\/main)$/);
|
|
85
|
+
if (!match || spec.startsWith('+')) throw Error('Only fast-forward pushes to main are supported; branches, tags and deletion are unavailable.');
|
|
86
|
+
target = match[2]; let state = await ask();
|
|
87
|
+
if (state.pending) { const settled = await waitForPush(await ask(`/push/${state.pending.operationId}`, { method:'POST' })); if (settled.state === 'pending') throw Error(`An earlier push is still resolving (${state.pending.operationId}). Retry later.`); state = await ask(); }
|
|
88
|
+
const head = git(['rev-parse', '--verify', `${match[1]}^{commit}`]).toString().trim();
|
|
89
|
+
try { git(['merge-base', '--is-ancestor', state.head, head]); } catch { throw Error('The remote branch changed. Fetch and merge before pushing.'); }
|
|
90
|
+
if (head === state.head) { say(`ok ${target}`); return; }
|
|
91
|
+
const objects = localObjects(head, state.head, state.limits.maxCommits), journal = pushReceipt(head, state.head);
|
|
92
|
+
let result;
|
|
93
|
+
try { result = await waitForPush(await ask('/push', { method:'POST', body:{ base:state.head, head, objects }, operationId:journal.receipt.operationId })); }
|
|
94
|
+
catch (error) {
|
|
95
|
+
// A lost response is not proof of failure. Ask the durable operation,
|
|
96
|
+
// retaining the same ID until the server confirms its outcome.
|
|
97
|
+
try { result = await waitForPush(await ask(`/push/${journal.receipt.operationId}`, { method:'POST' })); } catch { throw error; }
|
|
98
|
+
}
|
|
99
|
+
if (result.state === 'not-saved') { journal.clear(); throw Error('The interrupted push was cancelled before sending. Retry git push.'); }
|
|
100
|
+
if (result.state !== 'committed' || result.head !== head) throw Error(`Push still resolving (${journal.receipt.operationId}). Retry git push to reconcile it.`);
|
|
101
|
+
journal.clear(); say(`ok ${target}`);
|
|
102
|
+
} catch (error) { say(`error ${target || 'refs/heads/main'} ${cleanError(error)}`); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
(async () => {
|
|
106
|
+
const input = readline.createInterface({ input:process.stdin, crlfDelay:Infinity }), lines = input[Symbol.asyncIterator]();
|
|
107
|
+
for await (const line of lines) {
|
|
108
|
+
if (line === 'capabilities') { say('fetch'); say('push'); say('option'); say(''); }
|
|
109
|
+
else if (line.startsWith('option ')) say('unsupported');
|
|
110
|
+
else if (line === 'list' || line === 'list for-push') { advertised = await ask(); say('@refs/heads/main HEAD'); say(`${advertised.head} refs/heads/main`); say(''); }
|
|
111
|
+
else if (line.startsWith('fetch ')) {
|
|
112
|
+
const batch = [line]; for (;;) { const next = await lines.next(); if (next.done || !next.value) break; batch.push(next.value); }
|
|
113
|
+
await fetchObjects(batch.map(value => value.split(' ')[1])); say('');
|
|
114
|
+
} else if (line.startsWith('push ')) {
|
|
115
|
+
const batch = [line]; for (;;) { const next = await lines.next(); if (next.done || !next.value) break; batch.push(next.value); }
|
|
116
|
+
if (batch.length !== 1) { for (const item of batch) say(`error ${item.split(':').at(-1)} Push one main branch at a time.`); }
|
|
117
|
+
else await push(batch[0].slice(5)); say('');
|
|
118
|
+
} else if (!line) break;
|
|
119
|
+
else throw Error('Unsupported private Git helper command.');
|
|
120
|
+
}
|
|
121
|
+
})().catch(error => { process.stderr.write(`Clunkyard: ${cleanError(error)}\n`); process.exitCode = 1; process.stdin.destroy(); });
|
package/login.cjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { origin, saveCredential, request } = require('./client.cjs');
|
|
5
|
+
|
|
6
|
+
async function secret() {
|
|
7
|
+
if (!process.stdin.isTTY) {
|
|
8
|
+
let input = ''; for await (const data of process.stdin) { input += data; if (input.length > 2048) throw Error('Access key is too long.'); }
|
|
9
|
+
return input.trim();
|
|
10
|
+
}
|
|
11
|
+
process.stderr.write('Agent access key (hidden): ');
|
|
12
|
+
process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8');
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let value = '';
|
|
15
|
+
const finish = error => { process.stdin.removeListener('data', receive); process.stdin.setRawMode(false); process.stdin.pause(); process.stderr.write('\n'); error ? reject(error) : resolve(value); };
|
|
16
|
+
function receive(data) { for (const character of data) { if (character === '\u0003') return finish(Error('Sign-in cancelled.')); if (character === '\r' || character === '\n') return finish(); if (character === '\u007f' || character === '\b') value = value.slice(0, -1); else value += character; if (value.length > 1024) return finish(Error('Access key is too long.')); } }
|
|
17
|
+
process.stdin.on('data', receive);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function main() {
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (args.shift() !== 'login') throw Error('Usage: clunkyard-git login [--origin https://clunkyard.com] [--token-stdin]');
|
|
23
|
+
let site = 'https://clunkyard.com';
|
|
24
|
+
while (args.length) { const flag = args.shift(); if (flag === '--origin') site = args.shift(); else if (flag !== '--token-stdin') throw Error('Unknown sign-in option.'); }
|
|
25
|
+
site = origin(site); const key = await secret();
|
|
26
|
+
const me = await request(site, '/api/me', { key });
|
|
27
|
+
if (!me.account?.handle) throw Error('This access key does not identify a player.');
|
|
28
|
+
const file = saveCredential(site, key);
|
|
29
|
+
process.stdout.write(`Signed in as @${me.account.handle}. Credentials saved at ${file}.\n`);
|
|
30
|
+
}
|
|
31
|
+
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clunkyard/git",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Ordinary private Git for Clunkyard bot projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"git-remote-clunkyard": "git-remote-clunkyard",
|
|
11
|
+
"clunkyard-git": "login.cjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"git-remote-clunkyard",
|
|
15
|
+
"login.cjs",
|
|
16
|
+
"client.cjs",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/clunkyard/sdk.git",
|
|
23
|
+
"directory": "packages/git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://clunkyard.com/sdk/private-git",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/clunkyard/sdk/issues"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"clunkyard",
|
|
31
|
+
"git",
|
|
32
|
+
"robot",
|
|
33
|
+
"arena"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
}
|
|
42
|
+
}
|