@0xordek/git-me 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -112
- package/dist/cli.js +606 -26
- package/dist/worker.js +635 -0
- package/package.json +28 -23
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,389 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
|
|
6
|
+
// src/credentials.ts
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { platform } from "node:os";
|
|
9
|
+
var SERVICE = "git-me";
|
|
10
|
+
function createCredentialStore() {
|
|
11
|
+
return new SystemCredentialStore();
|
|
12
|
+
}
|
|
13
|
+
var SystemCredentialStore = class {
|
|
14
|
+
async get(key) {
|
|
15
|
+
if (platform() === "darwin") return await macGet(key);
|
|
16
|
+
if (platform() === "linux") return await linuxGet(key);
|
|
17
|
+
return await windowsGet(key);
|
|
18
|
+
}
|
|
19
|
+
async set(key, value) {
|
|
20
|
+
if (!value) throw new Error("credential cannot be empty");
|
|
21
|
+
if (platform() === "darwin") return await macSet(key, value);
|
|
22
|
+
if (platform() === "linux") return await linuxSet(key, value);
|
|
23
|
+
return await windowsSet(key, value);
|
|
24
|
+
}
|
|
25
|
+
async delete(key) {
|
|
26
|
+
if (platform() === "darwin") return await macDelete(key);
|
|
27
|
+
if (platform() === "linux") return await linuxDelete(key);
|
|
28
|
+
return await windowsDelete(key);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
async function macGet(key) {
|
|
32
|
+
const result = await run("security", ["find-generic-password", "-a", SERVICE, "-s", key, "-w"], void 0, true);
|
|
33
|
+
return result?.trim() || null;
|
|
34
|
+
}
|
|
35
|
+
async function macSet(key, value) {
|
|
36
|
+
await run("security", ["add-generic-password", "-U", "-a", SERVICE, "-s", key], value);
|
|
37
|
+
}
|
|
38
|
+
async function macDelete(key) {
|
|
39
|
+
await run("security", ["delete-generic-password", "-a", SERVICE, "-s", key], void 0, true);
|
|
40
|
+
}
|
|
41
|
+
async function linuxGet(key) {
|
|
42
|
+
const result = await run("secret-tool", ["lookup", "service", SERVICE, "profile", key], void 0, true);
|
|
43
|
+
return result?.trim() || null;
|
|
44
|
+
}
|
|
45
|
+
async function linuxSet(key, value) {
|
|
46
|
+
await run("secret-tool", ["store", "--label=git-me credential", "service", SERVICE, "profile", key], value);
|
|
47
|
+
}
|
|
48
|
+
async function linuxDelete(key) {
|
|
49
|
+
await run("secret-tool", ["clear", "service", SERVICE, "profile", key], void 0, true);
|
|
50
|
+
}
|
|
51
|
+
async function windowsGet(key) {
|
|
52
|
+
const script = `${windowsCredentialApi()}
|
|
53
|
+
$ptr = [IntPtr]::Zero
|
|
54
|
+
if ([WinCred]::CredRead($args[0], 1, 0, [ref]$ptr)) {
|
|
55
|
+
try {
|
|
56
|
+
$credential = [Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type][WinCred+CREDENTIAL])
|
|
57
|
+
$bytes = New-Object byte[] $credential.CredentialBlobSize
|
|
58
|
+
[Runtime.InteropServices.Marshal]::Copy($credential.CredentialBlob, $bytes, 0, $bytes.Length)
|
|
59
|
+
[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))
|
|
60
|
+
} finally { [WinCred]::CredFree($ptr) }
|
|
61
|
+
}`;
|
|
62
|
+
const result = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, key], void 0, true);
|
|
63
|
+
return result?.trim() || null;
|
|
64
|
+
}
|
|
65
|
+
async function windowsSet(key, value) {
|
|
66
|
+
const script = `${windowsCredentialApi()}
|
|
67
|
+
$value = [Console]::In.ReadToEnd()
|
|
68
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes($value)
|
|
69
|
+
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
|
|
70
|
+
try {
|
|
71
|
+
[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $ptr, $bytes.Length)
|
|
72
|
+
$credential = New-Object WinCred+CREDENTIAL
|
|
73
|
+
$credential.Type = 1
|
|
74
|
+
$credential.TargetName = $args[0]
|
|
75
|
+
$credential.UserName = 'git-me'
|
|
76
|
+
$credential.CredentialBlobSize = $bytes.Length
|
|
77
|
+
$credential.CredentialBlob = $ptr
|
|
78
|
+
$credential.Persist = 2
|
|
79
|
+
if (-not [WinCred]::CredWrite([ref]$credential, 0)) { throw 'CredWrite failed' }
|
|
80
|
+
} finally { [Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) }`;
|
|
81
|
+
await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, key], value);
|
|
82
|
+
}
|
|
83
|
+
async function windowsDelete(key) {
|
|
84
|
+
const script = `${windowsCredentialApi()}
|
|
85
|
+
[WinCred]::CredDelete($args[0], 1, 0) | Out-Null`;
|
|
86
|
+
await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, key], void 0, true);
|
|
87
|
+
}
|
|
88
|
+
function windowsCredentialApi() {
|
|
89
|
+
return `Add-Type @'
|
|
90
|
+
using System;
|
|
91
|
+
using System.Runtime.InteropServices;
|
|
92
|
+
public static class WinCred {
|
|
93
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
94
|
+
public struct CREDENTIAL {
|
|
95
|
+
public UInt32 Flags; public UInt32 Type; public string TargetName; public string Comment;
|
|
96
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
97
|
+
public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist;
|
|
98
|
+
public UInt32 AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName;
|
|
99
|
+
}
|
|
100
|
+
[DllImport("advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
101
|
+
public static extern bool CredWrite(ref CREDENTIAL credential, UInt32 flags);
|
|
102
|
+
[DllImport("advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
103
|
+
public static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
104
|
+
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
105
|
+
public static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
106
|
+
[DllImport("advapi32.dll", EntryPoint = "CredFree")]
|
|
107
|
+
public static extern void CredFree(IntPtr credential);
|
|
108
|
+
}
|
|
109
|
+
'@`;
|
|
110
|
+
}
|
|
111
|
+
function run(command, args, input, ignoreFailure = false) {
|
|
112
|
+
return new Promise((resolve2, reject) => {
|
|
113
|
+
const child = execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
|
|
114
|
+
if (error) {
|
|
115
|
+
if (ignoreFailure) return resolve2(null);
|
|
116
|
+
reject(new Error(`credential store unavailable: ${stderr.trim() || error.message}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
resolve2(stdout);
|
|
120
|
+
});
|
|
121
|
+
if (input !== void 0) {
|
|
122
|
+
child.stdin?.write(input);
|
|
123
|
+
child.stdin?.end();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/deploy.ts
|
|
129
|
+
import { randomBytes } from "node:crypto";
|
|
130
|
+
import { appendFile, mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
131
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
132
|
+
import { fileURLToPath } from "node:url";
|
|
133
|
+
import { createRequire } from "node:module";
|
|
134
|
+
import { spawn } from "node:child_process";
|
|
135
|
+
import { tmpdir } from "node:os";
|
|
136
|
+
|
|
137
|
+
// src/profile.ts
|
|
138
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
139
|
+
import { homedir, platform as platform2 } from "node:os";
|
|
140
|
+
import { dirname, join } from "node:path";
|
|
141
|
+
function createProfileStore(env = process.env) {
|
|
142
|
+
return new FileProfileStore(profileFilePath(env));
|
|
143
|
+
}
|
|
144
|
+
var FileProfileStore = class {
|
|
145
|
+
constructor(filePath) {
|
|
146
|
+
this.filePath = filePath;
|
|
147
|
+
}
|
|
148
|
+
filePath;
|
|
149
|
+
async get(name) {
|
|
150
|
+
const file = await readProfileFile(this.filePath);
|
|
151
|
+
const profile = file.profiles[name];
|
|
152
|
+
return profile && profile.name === name ? profile : null;
|
|
153
|
+
}
|
|
154
|
+
async save(profile) {
|
|
155
|
+
const file = await readProfileFile(this.filePath);
|
|
156
|
+
file.profiles[profile.name] = profile;
|
|
157
|
+
file.current = profile.name;
|
|
158
|
+
await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
|
|
159
|
+
const temporaryPath = `${this.filePath}.tmp`;
|
|
160
|
+
await writeFile(temporaryPath, `${JSON.stringify(file, null, 2)}
|
|
161
|
+
`, { mode: 384 });
|
|
162
|
+
await rename(temporaryPath, this.filePath);
|
|
163
|
+
await chmod(this.filePath, 384).catch(() => void 0);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
async function readProfileFile(filePath) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
169
|
+
if (parsed.version !== 1 || !parsed.profiles || typeof parsed.profiles !== "object") throw new Error(`invalid profile file: ${filePath}`);
|
|
170
|
+
return {
|
|
171
|
+
version: 1,
|
|
172
|
+
current: typeof parsed.current === "string" ? parsed.current : "default",
|
|
173
|
+
profiles: parsed.profiles
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.code === "ENOENT") return { version: 1, current: "default", profiles: {} };
|
|
177
|
+
if (error instanceof SyntaxError) throw new Error(`invalid profile file: ${filePath}`);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function profileFilePath(env) {
|
|
182
|
+
if (env.GITME_CONFIG_DIR) return join(env.GITME_CONFIG_DIR, "profiles.json");
|
|
183
|
+
if (platform2() === "win32" && env.APPDATA) return join(env.APPDATA, "git-me", "profiles.json");
|
|
184
|
+
if (platform2() === "darwin") return join(homedir(), "Library", "Application Support", "git-me", "profiles.json");
|
|
185
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "git-me", "profiles.json");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/deploy.ts
|
|
189
|
+
var COMPATIBILITY_DATE = "2026-07-07";
|
|
190
|
+
var require2 = createRequire(import.meta.url);
|
|
191
|
+
async function deployWorker(options, deps = {}) {
|
|
192
|
+
const runCommand = deps.runCommand ?? runWrangler;
|
|
193
|
+
const profileStore = deps.profileStore ?? createProfileStore();
|
|
194
|
+
const credentialStore = deps.credentialStore ?? createCredentialStore();
|
|
195
|
+
const createTempDirectory = deps.createTempDirectory ?? (() => mkdtemp(join2(tmpdir(), "git-me-deploy-")));
|
|
196
|
+
const generateSecret = deps.generateSecret ?? (() => randomBytes(32).toString("base64url"));
|
|
197
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
198
|
+
const workerName = options.workerName || defaultWorkerName();
|
|
199
|
+
const workerNameGenerated = !options.workerName;
|
|
200
|
+
const bucketName = `${workerName}-objects`;
|
|
201
|
+
const kvName = `${workerName}-metadata`;
|
|
202
|
+
const adminSecret = options.adminSecret ?? generateSecret();
|
|
203
|
+
if (await profileStore.get(options.profile)) throw new Error(`profile already exists: ${options.profile}`);
|
|
204
|
+
let storedCredential = false;
|
|
205
|
+
if (!options.adminSecret) {
|
|
206
|
+
try {
|
|
207
|
+
await credentialStore.set(credentialKey(options.profile), adminSecret);
|
|
208
|
+
storedCredential = true;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw new Error(`${message}; re-run with --token-stdin if the OS credential store is unavailable`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const tempDirectory = await createTempDirectory();
|
|
215
|
+
const configPath = join2(tempDirectory, "wrangler.toml");
|
|
216
|
+
const workerBundle = deps.workerBundle ?? defaultWorkerBundle();
|
|
217
|
+
let bucketCreated = false;
|
|
218
|
+
let kvCreated = false;
|
|
219
|
+
let workerDeployed = false;
|
|
220
|
+
let kvNamespaceId;
|
|
221
|
+
try {
|
|
222
|
+
await writeFile2(configPath, initialConfig(workerName, workerBundle));
|
|
223
|
+
await runCommand(["login"], { interactive: true });
|
|
224
|
+
const whoami = await runCommand(["whoami"]);
|
|
225
|
+
const accountId = options.accountId ?? accountIdFromOutput(`${whoami.stdout}
|
|
226
|
+
${whoami.stderr}`);
|
|
227
|
+
if (!accountId) throw new Error("Cloudflare did not report an account ID");
|
|
228
|
+
await writeFile2(configPath, initialConfig(workerName, workerBundle, accountId));
|
|
229
|
+
await runCommand(["r2", "bucket", "create", bucketName, "--config", configPath]);
|
|
230
|
+
bucketCreated = true;
|
|
231
|
+
await appendFile(configPath, r2Binding(bucketName));
|
|
232
|
+
const kvOutput = await runCommand(["kv", "namespace", "create", kvName, "--config", configPath]);
|
|
233
|
+
kvCreated = true;
|
|
234
|
+
kvNamespaceId = kvNamespaceIdFromOutput(`${kvOutput.stdout}
|
|
235
|
+
${kvOutput.stderr}`);
|
|
236
|
+
if (!kvNamespaceId) throw new Error("Cloudflare did not report a KV namespace ID");
|
|
237
|
+
await appendFile(configPath, kvBinding(kvNamespaceId));
|
|
238
|
+
const deployOutput = await runCommand(["deploy", "--config", configPath, "--no-bundle"]);
|
|
239
|
+
workerDeployed = true;
|
|
240
|
+
const endpoint = endpointFromOutput(`${deployOutput.stdout}
|
|
241
|
+
${deployOutput.stderr}`);
|
|
242
|
+
if (!endpoint) throw new Error("Cloudflare did not report a Workers URL after deploy");
|
|
243
|
+
await runCommand(["secret", "put", "GITME_AUTH_TOKEN", "--config", configPath], { input: adminSecret });
|
|
244
|
+
await waitForHealth(endpoint, fetchImpl);
|
|
245
|
+
let warning;
|
|
246
|
+
if (options.adminSecret) {
|
|
247
|
+
try {
|
|
248
|
+
await credentialStore.set(credentialKey(options.profile), adminSecret);
|
|
249
|
+
} catch {
|
|
250
|
+
warning = "Admin credential was not saved to the OS credential store; use --token-stdin for user commands.";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const result = {
|
|
254
|
+
profile: options.profile,
|
|
255
|
+
endpoint,
|
|
256
|
+
workerName,
|
|
257
|
+
accountId,
|
|
258
|
+
bucketName,
|
|
259
|
+
kvNamespaceId,
|
|
260
|
+
warning
|
|
261
|
+
};
|
|
262
|
+
const profile = {
|
|
263
|
+
name: options.profile,
|
|
264
|
+
endpoint,
|
|
265
|
+
workerName,
|
|
266
|
+
accountId: result.accountId,
|
|
267
|
+
bucketName,
|
|
268
|
+
kvNamespaceId,
|
|
269
|
+
createdAt: (deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))()
|
|
270
|
+
};
|
|
271
|
+
await profileStore.save(profile);
|
|
272
|
+
return result;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (storedCredential) await credentialStore.delete(credentialKey(options.profile)).catch(() => void 0);
|
|
275
|
+
const cleanup = [];
|
|
276
|
+
if (workerDeployed && workerNameGenerated) await runCommand(["delete", workerName, "--config", configPath, "--force"]).catch(() => cleanup.push(`Worker ${workerName}`));
|
|
277
|
+
else if (workerDeployed) cleanup.push(`Worker ${workerName}`);
|
|
278
|
+
if (kvCreated && kvNamespaceId) await runCommand(["kv", "namespace", "delete", kvNamespaceId, "--config", configPath]).catch(() => cleanup.push(`KV namespace ${kvName}`));
|
|
279
|
+
else if (kvCreated) cleanup.push(`KV namespace ${kvName}`);
|
|
280
|
+
if (bucketCreated) await runCommand(["r2", "bucket", "delete", bucketName, "--config", configPath]).catch(() => cleanup.push(`R2 bucket ${bucketName}`));
|
|
281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
282
|
+
throw new Error(cleanup.length > 0 ? `${message}. Resources to verify: ${cleanup.join(", ")}` : message);
|
|
283
|
+
} finally {
|
|
284
|
+
await rm(tempDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function credentialKey(profile) {
|
|
288
|
+
return `git-me:${profile}:admin`;
|
|
289
|
+
}
|
|
290
|
+
function defaultWorkerName() {
|
|
291
|
+
return `git-me-${randomBytes(4).toString("hex")}`;
|
|
292
|
+
}
|
|
293
|
+
function defaultWorkerBundle() {
|
|
294
|
+
return resolve(dirname2(fileURLToPath(import.meta.url)), "worker.js");
|
|
295
|
+
}
|
|
296
|
+
function initialConfig(workerName, workerBundle, accountId) {
|
|
297
|
+
return [
|
|
298
|
+
`name = ${tomlString(workerName)}`,
|
|
299
|
+
`main = ${tomlString(workerBundle)}`,
|
|
300
|
+
...accountId ? [`account_id = ${tomlString(accountId)}`] : [],
|
|
301
|
+
`compatibility_date = ${tomlString(COMPATIBILITY_DATE)}`,
|
|
302
|
+
"workers_dev = true",
|
|
303
|
+
"",
|
|
304
|
+
"[[durable_objects.bindings]]",
|
|
305
|
+
'name = "GITME_AUTH"',
|
|
306
|
+
'class_name = "AuthUser"',
|
|
307
|
+
"",
|
|
308
|
+
"[[migrations]]",
|
|
309
|
+
'tag = "v1"',
|
|
310
|
+
'new_sqlite_classes = ["AuthUser"]',
|
|
311
|
+
""
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function tomlString(value) {
|
|
315
|
+
return `"${value.replaceAll("\\", "/").replaceAll('"', '\\"')}"`;
|
|
316
|
+
}
|
|
317
|
+
function r2Binding(bucketName) {
|
|
318
|
+
return `
|
|
319
|
+
[[r2_buckets]]
|
|
320
|
+
binding = "GITME_R2"
|
|
321
|
+
bucket_name = ${tomlString(bucketName)}
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
function kvBinding(namespaceId) {
|
|
325
|
+
return `
|
|
326
|
+
[[kv_namespaces]]
|
|
327
|
+
binding = "GITME_KV"
|
|
328
|
+
id = ${tomlString(namespaceId)}
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
function kvNamespaceIdFromOutput(output) {
|
|
332
|
+
return output.match(/\bid\s*=\s*"([0-9a-f]{32})"/i)?.[1];
|
|
333
|
+
}
|
|
334
|
+
function endpointFromOutput(output) {
|
|
335
|
+
const field = output.match(/"(?:url|workers_dev_url)"\s*:\s*"(https:\/\/[^"\\]+)"/i)?.[1];
|
|
336
|
+
const match = field ? [field] : output.match(/https:\/\/[a-z0-9][a-z0-9.-]*\.workers\.dev(?:\/[^\s]*)?/i);
|
|
337
|
+
if (!match) return void 0;
|
|
338
|
+
return match[1] ? match[1].replace(/[),.]+$/, "") : match[0].replace(/[),.]+$/, "");
|
|
339
|
+
}
|
|
340
|
+
function accountIdFromOutput(output) {
|
|
341
|
+
return output.match(/\b[0-9a-f]{32}\b/i)?.[0];
|
|
342
|
+
}
|
|
343
|
+
async function waitForHealth(endpoint, fetchImpl) {
|
|
344
|
+
let lastError = "health check failed";
|
|
345
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
346
|
+
try {
|
|
347
|
+
const response = await fetchImpl(new URL("/health", endpoint));
|
|
348
|
+
if (response.ok) {
|
|
349
|
+
const body = await response.json();
|
|
350
|
+
if (body.ok === true) return;
|
|
351
|
+
}
|
|
352
|
+
lastError = `health check returned ${response.status}`;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
355
|
+
}
|
|
356
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1e3));
|
|
357
|
+
}
|
|
358
|
+
throw new Error(lastError);
|
|
359
|
+
}
|
|
360
|
+
async function runWrangler(args, options) {
|
|
361
|
+
const executable = process.env.GITME_WRANGLER_BIN || require2.resolve("wrangler");
|
|
362
|
+
return await spawnCommand(process.execPath, [executable, ...args], options);
|
|
363
|
+
}
|
|
364
|
+
async function spawnCommand(command, args, options = {}) {
|
|
365
|
+
return await new Promise((resolvePromise, reject) => {
|
|
366
|
+
const child = spawn(command, args, { stdio: options.interactive ? "inherit" : "pipe" });
|
|
367
|
+
let stdout = "";
|
|
368
|
+
let stderr = "";
|
|
369
|
+
if (!options.interactive) {
|
|
370
|
+
child.stdout?.on("data", (chunk) => {
|
|
371
|
+
stdout += String(chunk);
|
|
372
|
+
});
|
|
373
|
+
child.stderr?.on("data", (chunk) => {
|
|
374
|
+
stderr += String(chunk);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (options.input !== void 0) {
|
|
378
|
+
child.stdin?.write(options.input);
|
|
379
|
+
child.stdin?.end();
|
|
380
|
+
}
|
|
381
|
+
child.once("error", reject);
|
|
382
|
+
child.once("close", (code) => {
|
|
383
|
+
if (code === 0) resolvePromise({ stdout, stderr });
|
|
384
|
+
else reject(new Error(stderr.trim() || `Cloudflare command failed with exit code ${code ?? "unknown"}`));
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
6
389
|
// src/lfs-client.ts
|
|
7
390
|
var LFS_JSON = "application/vnd.git-lfs+json";
|
|
8
391
|
var ERROR_SNIPPET_BYTES = 200;
|
|
@@ -102,14 +485,14 @@ async function scanPointers(repoPath) {
|
|
|
102
485
|
}
|
|
103
486
|
async function listTrackedFiles(repoPath) {
|
|
104
487
|
const childProcess = await nodeImport2("node:child_process");
|
|
105
|
-
const stdout = await new Promise((
|
|
488
|
+
const stdout = await new Promise((resolve2, reject) => {
|
|
106
489
|
childProcess.execFile("git", ["-C", repoPath, "ls-files", "-z"], { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }, (error, output, stderr) => {
|
|
107
490
|
if (error) {
|
|
108
491
|
reject(new Error(`${error.message}
|
|
109
492
|
${stderr}`));
|
|
110
493
|
return;
|
|
111
494
|
}
|
|
112
|
-
|
|
495
|
+
resolve2(output);
|
|
113
496
|
});
|
|
114
497
|
});
|
|
115
498
|
return stdout.split("\0").filter((trackedPath) => trackedPath.length > 0);
|
|
@@ -214,10 +597,10 @@ async function sha256File(path) {
|
|
|
214
597
|
const [fs, cryptoModule] = await Promise.all([nodeImport3("node:fs"), nodeImport3("node:crypto")]);
|
|
215
598
|
const hash = cryptoModule.createHash("sha256");
|
|
216
599
|
const stream = fs.createReadStream(path);
|
|
217
|
-
return await new Promise((
|
|
600
|
+
return await new Promise((resolve2, reject) => {
|
|
218
601
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
219
602
|
stream.on("error", reject);
|
|
220
|
-
stream.on("end", () =>
|
|
603
|
+
stream.on("end", () => resolve2(hash.digest("hex")));
|
|
221
604
|
});
|
|
222
605
|
}
|
|
223
606
|
async function defaultCreateTempPath() {
|
|
@@ -236,14 +619,14 @@ async function setGitConfigValue(repoPath, key, value) {
|
|
|
236
619
|
}
|
|
237
620
|
async function execGit(repoPath, args) {
|
|
238
621
|
const childProcess = await nodeImport3("node:child_process");
|
|
239
|
-
return await new Promise((
|
|
622
|
+
return await new Promise((resolve2, reject) => {
|
|
240
623
|
childProcess.execFile("git", ["-C", repoPath, ...args], { encoding: "utf8" }, (error, stdout, stderr) => {
|
|
241
624
|
if (error) {
|
|
242
625
|
reject(new Error(`${error.message}
|
|
243
626
|
${stderr}`));
|
|
244
627
|
return;
|
|
245
628
|
}
|
|
246
|
-
|
|
629
|
+
resolve2(stdout);
|
|
247
630
|
});
|
|
248
631
|
});
|
|
249
632
|
}
|
|
@@ -262,6 +645,7 @@ async function runCli(argv, io = {}) {
|
|
|
262
645
|
const [command, ...args] = argv;
|
|
263
646
|
if (command === "migrate") return await runMigrate(args, { ...io, stdout: out, stderr: err });
|
|
264
647
|
if (command === "user") return await runUser(args, { ...io, stdout: out, stderr: err });
|
|
648
|
+
if (command === "worker") return await runWorker(args, { ...io, stdout: out, stderr: err });
|
|
265
649
|
err(`unknown command: ${command}
|
|
266
650
|
|
|
267
651
|
${topLevelUsage()}`);
|
|
@@ -272,16 +656,57 @@ async function runUser(args, io) {
|
|
|
272
656
|
io.stdout(userUsage());
|
|
273
657
|
return 0;
|
|
274
658
|
}
|
|
275
|
-
|
|
659
|
+
let parsed;
|
|
660
|
+
try {
|
|
661
|
+
parsed = await parseUserArgs(args, io);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}
|
|
664
|
+
|
|
665
|
+
${userUsage()}`);
|
|
666
|
+
return 2;
|
|
667
|
+
}
|
|
276
668
|
if ("error" in parsed) {
|
|
277
669
|
io.stderr(`${parsed.error}
|
|
278
670
|
|
|
279
671
|
${userUsage()}`);
|
|
280
672
|
return 2;
|
|
281
673
|
}
|
|
674
|
+
if (parsed.options.action === "delete" && !parsed.options.yes && (io.confirm || !io.userRequest)) {
|
|
675
|
+
const confirmed = await (io.confirm ?? confirmPrompt)(`Delete user "${parsed.options.username}"? [y/N] `);
|
|
676
|
+
if (!confirmed) {
|
|
677
|
+
io.stdout("Cancelled.\n");
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
282
681
|
try {
|
|
283
682
|
const result = await (io.userRequest ?? requestUser)(parsed.options);
|
|
284
|
-
io.stdout(formatUserResult(result));
|
|
683
|
+
io.stdout(formatUserResult(result, parsed.options.json === true));
|
|
684
|
+
return 0;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}
|
|
687
|
+
`);
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function runWorker(args, io) {
|
|
692
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
693
|
+
io.stdout(workerUsage());
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
const parsed = await parseWorkerArgs(args, io);
|
|
697
|
+
if ("error" in parsed) {
|
|
698
|
+
io.stderr(`${parsed.error}
|
|
699
|
+
|
|
700
|
+
${workerUsage()}`);
|
|
701
|
+
return 2;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const result = await (io.workerDeploy ?? deployWorker)(parsed.options);
|
|
705
|
+
io.stdout(`Deployed: ${result.endpoint}
|
|
706
|
+
Profile: ${result.profile}
|
|
707
|
+
LFS URL: ${result.endpoint}
|
|
708
|
+
${result.warning ? `Warning: ${result.warning}
|
|
709
|
+
` : ""}`);
|
|
285
710
|
return 0;
|
|
286
711
|
} catch (error) {
|
|
287
712
|
io.stderr(`${error instanceof Error ? error.message : String(error)}
|
|
@@ -313,14 +738,22 @@ ${migrateUsage()}`);
|
|
|
313
738
|
}
|
|
314
739
|
async function parseUserArgs(args, io) {
|
|
315
740
|
const action = args[0];
|
|
316
|
-
if (action !== "add" && action !== "delete") return { error: "missing user action: add or delete" };
|
|
741
|
+
if (action !== "add" && action !== "delete" && action !== "list") return { error: "missing user action: add, list, or delete" };
|
|
317
742
|
let targetUrl = "";
|
|
318
743
|
let token;
|
|
319
744
|
let username = "";
|
|
320
745
|
let password;
|
|
321
|
-
let access = "";
|
|
746
|
+
let access = action === "add" ? "read" : "";
|
|
747
|
+
let profileName = "default";
|
|
748
|
+
let yes = false;
|
|
749
|
+
let jsonOutput = false;
|
|
322
750
|
for (let index = 1; index < args.length; index += 1) {
|
|
323
751
|
const arg = args[index];
|
|
752
|
+
if (!arg.startsWith("--")) {
|
|
753
|
+
if (username) return { error: "duplicate username" };
|
|
754
|
+
username = arg;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
324
757
|
if (arg === "--token-stdin") {
|
|
325
758
|
if (token) return { error: "duplicate token source" };
|
|
326
759
|
token = { stdin: true };
|
|
@@ -331,6 +764,14 @@ async function parseUserArgs(args, io) {
|
|
|
331
764
|
password = { stdin: true };
|
|
332
765
|
continue;
|
|
333
766
|
}
|
|
767
|
+
if (arg === "--yes") {
|
|
768
|
+
yes = true;
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (arg === "--json") {
|
|
772
|
+
jsonOutput = true;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
334
775
|
const value = args[index + 1];
|
|
335
776
|
if (!value || value.startsWith("--")) return { error: `missing value for ${arg}` };
|
|
336
777
|
index += 1;
|
|
@@ -346,24 +787,41 @@ async function parseUserArgs(args, io) {
|
|
|
346
787
|
const source = envSecret(value);
|
|
347
788
|
if (!source) return { error: `invalid environment variable name: ${value}` };
|
|
348
789
|
password = source;
|
|
349
|
-
} else if (arg === "--access")
|
|
790
|
+
} else if (arg === "--access") {
|
|
791
|
+
access = value;
|
|
792
|
+
} else if (arg === "--profile") profileName = value;
|
|
350
793
|
else return { error: `unknown option: ${arg}` };
|
|
351
794
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
if (
|
|
795
|
+
const explicitTarget = Boolean(targetUrl);
|
|
796
|
+
const profileStore = io.profileStore ?? createProfileStore(io.env ?? process.env);
|
|
797
|
+
const profile = explicitTarget ? null : await profileStore.get(profileName);
|
|
798
|
+
if (!targetUrl) targetUrl = profile?.endpoint || "";
|
|
799
|
+
if (!token && profile) {
|
|
800
|
+
const credentialStore = io.credentialStore ?? createCredentialStore();
|
|
801
|
+
const storedToken = await credentialStore.get(credentialKey(profileName));
|
|
802
|
+
if (storedToken) token = { value: storedToken };
|
|
803
|
+
}
|
|
804
|
+
if (!targetUrl) return { error: "missing profile; deploy a worker first or provide --target" };
|
|
805
|
+
if (!token) return { error: "missing admin credential; provide --token-env or --token-stdin" };
|
|
806
|
+
if (action !== "list" && !username) return { error: "missing username" };
|
|
807
|
+
if (action === "add" && access !== "read" && access !== "write") return { error: "invalid --access read|write" };
|
|
357
808
|
if (isStdinSecret(token) && password && isStdinSecret(password)) return { error: "only one secret may use standard input" };
|
|
358
809
|
try {
|
|
810
|
+
let resolvedPassword;
|
|
811
|
+
if (action === "add") {
|
|
812
|
+
resolvedPassword = password ? await readSecret(password, io) : await (io.readPassword ?? readPassword)("Password: ");
|
|
813
|
+
if (!resolvedPassword) return { error: "password is empty" };
|
|
814
|
+
}
|
|
359
815
|
return {
|
|
360
816
|
options: {
|
|
361
817
|
action,
|
|
362
818
|
targetUrl,
|
|
363
819
|
token: await readSecret(token, io),
|
|
364
|
-
username,
|
|
365
|
-
password:
|
|
366
|
-
access: access || void 0
|
|
820
|
+
username: username || void 0,
|
|
821
|
+
password: resolvedPassword,
|
|
822
|
+
access: access || void 0,
|
|
823
|
+
...yes ? { yes: true } : {},
|
|
824
|
+
...jsonOutput ? { json: true } : {}
|
|
367
825
|
}
|
|
368
826
|
};
|
|
369
827
|
} catch (error) {
|
|
@@ -371,9 +829,10 @@ async function parseUserArgs(args, io) {
|
|
|
371
829
|
}
|
|
372
830
|
}
|
|
373
831
|
async function requestUser(options) {
|
|
374
|
-
const
|
|
832
|
+
const path = options.action === "list" ? "/admin/users" : `/admin/users/${encodeURIComponent(options.username || "")}`;
|
|
833
|
+
const url = new URL(path, options.targetUrl.endsWith("/") ? options.targetUrl : `${options.targetUrl}/`);
|
|
375
834
|
const res = await fetch(url, {
|
|
376
|
-
method: options.action === "add" ? "PUT" : "DELETE",
|
|
835
|
+
method: options.action === "add" ? "PUT" : options.action === "delete" ? "DELETE" : "GET",
|
|
377
836
|
headers: {
|
|
378
837
|
Authorization: `Bearer ${options.token}`,
|
|
379
838
|
...options.action === "add" ? { "Content-Type": "application/json" } : {}
|
|
@@ -382,7 +841,45 @@ async function requestUser(options) {
|
|
|
382
841
|
});
|
|
383
842
|
const text = await res.text();
|
|
384
843
|
if (!res.ok) throw new Error(text.trim() || `request failed: ${res.status}`);
|
|
385
|
-
|
|
844
|
+
const result = JSON.parse(text);
|
|
845
|
+
if (options.action === "list" && !Array.isArray(result.users)) throw new Error("invalid user list response");
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
async function parseWorkerArgs(args, io) {
|
|
849
|
+
if (args[0] !== "deploy") return { error: "missing worker action: deploy" };
|
|
850
|
+
let profile = "default";
|
|
851
|
+
let workerName;
|
|
852
|
+
let accountId;
|
|
853
|
+
let adminToken;
|
|
854
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
855
|
+
const arg = args[index];
|
|
856
|
+
if (arg === "--token-stdin") {
|
|
857
|
+
if (adminToken) return { error: "duplicate admin credential source" };
|
|
858
|
+
adminToken = { stdin: true };
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const value = args[index + 1];
|
|
862
|
+
if (!value || value.startsWith("--")) return { error: `missing value for ${arg}` };
|
|
863
|
+
index += 1;
|
|
864
|
+
if (arg === "--profile") profile = value;
|
|
865
|
+
else if (arg === "--name") workerName = value;
|
|
866
|
+
else if (arg === "--account-id") {
|
|
867
|
+
if (!/^[0-9a-f]{32}$/i.test(value)) return { error: "invalid --account-id" };
|
|
868
|
+
accountId = value;
|
|
869
|
+
} else if (arg === "--token-env") {
|
|
870
|
+
if (adminToken) return { error: "duplicate admin credential source" };
|
|
871
|
+
const source = envSecret(value);
|
|
872
|
+
if (!source) return { error: `invalid environment variable name: ${value}` };
|
|
873
|
+
adminToken = source;
|
|
874
|
+
} else return { error: `unknown option: ${arg}` };
|
|
875
|
+
}
|
|
876
|
+
let adminSecret;
|
|
877
|
+
try {
|
|
878
|
+
if (adminToken) adminSecret = await readSecret(adminToken, io);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
881
|
+
}
|
|
882
|
+
return { action: "deploy", options: { profile, workerName, accountId, adminSecret } };
|
|
386
883
|
}
|
|
387
884
|
async function parseMigrateArgs(args, defaultRepoPath, io) {
|
|
388
885
|
const sourceHeaderSources = [];
|
|
@@ -449,6 +946,7 @@ function isStdinSecret(source) {
|
|
|
449
946
|
return "stdin" in source;
|
|
450
947
|
}
|
|
451
948
|
async function readSecret(source, io) {
|
|
949
|
+
if ("value" in source) return source.value;
|
|
452
950
|
if ("env" in source) {
|
|
453
951
|
const value2 = (io.env ?? process.env)[source.env];
|
|
454
952
|
if (!value2) throw new Error(`missing environment variable: ${source.env}`);
|
|
@@ -475,8 +973,9 @@ function topLevelUsage() {
|
|
|
475
973
|
return `Usage: git-me <command>
|
|
476
974
|
|
|
477
975
|
Commands:
|
|
478
|
-
|
|
976
|
+
worker deploy a zero-config git-me Worker
|
|
479
977
|
user manage git-me LFS users
|
|
978
|
+
migrate migrate Git LFS objects to git-me
|
|
480
979
|
`;
|
|
481
980
|
}
|
|
482
981
|
function migrateUsage() {
|
|
@@ -492,12 +991,30 @@ Options:
|
|
|
492
991
|
`;
|
|
493
992
|
}
|
|
494
993
|
function userUsage() {
|
|
495
|
-
return `Usage: git-me user <add|delete>
|
|
994
|
+
return `Usage: git-me user <add|delete> [username] [options]
|
|
995
|
+
git-me user list [options]
|
|
496
996
|
|
|
497
997
|
Options:
|
|
998
|
+
--profile <name> local profile (default: default)
|
|
999
|
+
--target <url> Worker URL (default: saved profile)
|
|
1000
|
+
--token-env <name> env var containing admin token
|
|
1001
|
+
--token-stdin read admin token from standard input
|
|
498
1002
|
--password-env <name> env var containing password for add
|
|
499
1003
|
--password-stdin read password for add from standard input
|
|
500
|
-
--access <read|write> access for add
|
|
1004
|
+
--access <read|write> access for add (default: read)
|
|
1005
|
+
--yes skip delete confirmation
|
|
1006
|
+
--json output user list as JSON
|
|
1007
|
+
`;
|
|
1008
|
+
}
|
|
1009
|
+
function workerUsage() {
|
|
1010
|
+
return `Usage: git-me worker deploy [options]
|
|
1011
|
+
|
|
1012
|
+
Options:
|
|
1013
|
+
--profile <name> local profile name (default: default)
|
|
1014
|
+
--name <name> Worker name (default: generated)
|
|
1015
|
+
--account-id <id> Cloudflare account ID
|
|
1016
|
+
--token-stdin use an admin secret from standard input
|
|
1017
|
+
--token-env <name> use an admin secret from an environment variable
|
|
501
1018
|
`;
|
|
502
1019
|
}
|
|
503
1020
|
function formatResult(result) {
|
|
@@ -506,12 +1023,75 @@ function formatResult(result) {
|
|
|
506
1023
|
return `${lines.join("\n")}
|
|
507
1024
|
`;
|
|
508
1025
|
}
|
|
509
|
-
function formatUserResult(result) {
|
|
1026
|
+
function formatUserResult(result, jsonOutput = false) {
|
|
1027
|
+
if (result.users) {
|
|
1028
|
+
if (jsonOutput) return `${JSON.stringify(result.users)}
|
|
1029
|
+
`;
|
|
1030
|
+
if (result.users.length === 0) return "USERNAME ACCESS\n";
|
|
1031
|
+
const width = Math.max("USERNAME".length, ...result.users.map((user) => user.username.length));
|
|
1032
|
+
return [`${"USERNAME".padEnd(width)} ACCESS`, ...result.users.map((user) => `${user.username.padEnd(width)} ${user.access}`)].join("\n") + "\n";
|
|
1033
|
+
}
|
|
510
1034
|
if (result.deleted) return `username=${result.username} deleted=true
|
|
511
1035
|
`;
|
|
512
1036
|
return `username=${result.username} access=${result.access}
|
|
513
1037
|
`;
|
|
514
1038
|
}
|
|
1039
|
+
async function confirmPrompt(prompt) {
|
|
1040
|
+
const value = await ioReadLine(prompt);
|
|
1041
|
+
return /^y(?:es)?$/i.test(value.trim());
|
|
1042
|
+
}
|
|
1043
|
+
async function ioReadLine(prompt) {
|
|
1044
|
+
const readline = await import("node:readline/promises");
|
|
1045
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1046
|
+
try {
|
|
1047
|
+
return await rl.question(prompt);
|
|
1048
|
+
} finally {
|
|
1049
|
+
rl.close();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
async function readPassword(prompt) {
|
|
1053
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("password prompt unavailable; use --password-stdin");
|
|
1054
|
+
const stdin = process.stdin;
|
|
1055
|
+
const stdout = process.stdout;
|
|
1056
|
+
stdout.write(prompt);
|
|
1057
|
+
stdin.setRawMode(true);
|
|
1058
|
+
stdin.resume();
|
|
1059
|
+
return await new Promise((resolve2, reject) => {
|
|
1060
|
+
const bytes = [];
|
|
1061
|
+
const onData = (chunk) => {
|
|
1062
|
+
for (const byte of chunk) {
|
|
1063
|
+
if (byte === 3) {
|
|
1064
|
+
cleanup();
|
|
1065
|
+
reject(new Error("password prompt cancelled"));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (byte === 13 || byte === 10) {
|
|
1069
|
+
cleanup();
|
|
1070
|
+
stdout.write("\n");
|
|
1071
|
+
resolve2(Buffer.from(bytes).toString("utf8"));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (byte === 127 || byte === 8) {
|
|
1075
|
+
removeLastUtf8CodePoint(bytes);
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
bytes.push(byte);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
const cleanup = () => {
|
|
1082
|
+
stdin.off("data", onData);
|
|
1083
|
+
stdin.setRawMode(false);
|
|
1084
|
+
stdin.pause();
|
|
1085
|
+
};
|
|
1086
|
+
stdin.on("data", onData);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
function removeLastUtf8CodePoint(bytes) {
|
|
1090
|
+
if (bytes.length === 0) return;
|
|
1091
|
+
let index = bytes.length - 1;
|
|
1092
|
+
while (index > 0 && (bytes[index] & 192) === 128) index -= 1;
|
|
1093
|
+
bytes.splice(index);
|
|
1094
|
+
}
|
|
515
1095
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
516
1096
|
runCli(process.argv.slice(2)).then((code) => {
|
|
517
1097
|
process.exitCode = code;
|