@minnovate/shadow-publisher 0.10.3 → 0.11.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-publisher",
|
|
3
3
|
"displayName": "Git Publisher",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "Publish an app folder's Git history to the serverless Git publishing backend and keep it updated (\"publish my app\", \"update my app\", \"fetch the engineered version\").",
|
|
6
6
|
"author": { "name": "git-publisher project" },
|
|
7
7
|
"keywords": ["git", "publish", "deploy", "serverless"]
|
|
@@ -10,6 +10,9 @@ You publish the user's app folder with the `publisher` command. All saving, merg
|
|
|
10
10
|
retry and resume logic lives in it; your job is to run it, read its `--json` output, and tell the
|
|
11
11
|
user what happened in plain language. Never explain S3, packs, commits, sessions or tokens.
|
|
12
12
|
|
|
13
|
+
**When the user asks for Shadow or the publisher, use it at once.** Do not look at other ways to
|
|
14
|
+
host or share the app first.
|
|
15
|
+
|
|
13
16
|
**`publisher sync` is how a change is saved and published. Never run `git add`, `git commit`,
|
|
14
17
|
`git push` or `git stash` in the user's app folder**, and never add a git remote. `sync` records the
|
|
15
18
|
folder's changes itself — and leaves out `.env` files, keys, `node_modules` and anything larger than
|
|
@@ -25,10 +28,25 @@ Every command accepts `--json` and prints exactly one JSON object on stdout. Exi
|
|
|
25
28
|
publisher --version
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
If the command is not found,
|
|
29
|
-
Node.js 22). In a Claude Cowork task
|
|
30
|
-
|
|
31
|
-
publisher. If `npm` itself is missing,
|
|
31
|
+
If the command is not found, or the version is older than 0.11.0, install it:
|
|
32
|
+
`npm install -g @minnovate/shadow-publisher@latest` (it needs Node.js 22). In a Claude Cowork task
|
|
33
|
+
this is normal: the sandbox starts empty every time, so install it without asking. On the person's
|
|
34
|
+
own computer, say in one line that you are installing the publisher. If `npm` itself is missing,
|
|
35
|
+
tell them Node.js 22 must be installed first, and stop.
|
|
36
|
+
|
|
37
|
+
### In a sandbox: keep the sign-in where the next command finds it
|
|
38
|
+
|
|
39
|
+
A sandbox (Claude Cowork, Claude Code on the web, a chat's code tool) can forget every file outside
|
|
40
|
+
its working folder between two commands. A sign-in kept in `/root/.config` is then gone at the
|
|
41
|
+
next command. So in a sandbox, do this from the start:
|
|
42
|
+
|
|
43
|
+
- Put `GIT_PUBLISHER_CREDENTIALS=<working folder>/.shadow-publisher/credentials.json` in front of
|
|
44
|
+
**every** `publisher` command. Use the absolute path of the folder the task works in. Never use
|
|
45
|
+
`/root` or `/tmp`, and never a path inside the app folder.
|
|
46
|
+
- Do nothing about `TMPDIR`. From 0.11.0 the publisher uses `/tmp` when `TMPDIR` names a folder that
|
|
47
|
+
does not exist, and says so in one line.
|
|
48
|
+
|
|
49
|
+
On the person's own computer, use neither: the default place is kept.
|
|
32
50
|
|
|
33
51
|
```bash
|
|
34
52
|
publisher doctor --json
|
|
@@ -49,25 +67,30 @@ publisher doctor --json
|
|
|
49
67
|
|
|
50
68
|
### Signing in
|
|
51
69
|
|
|
52
|
-
`publisher login` waits until the
|
|
53
|
-
|
|
54
|
-
its output going to a file, and read the file:
|
|
70
|
+
`publisher login` waits until the code it prints is approved, and your shell may show nothing until
|
|
71
|
+
a command ends. So sign in with two short commands instead of one that waits:
|
|
55
72
|
|
|
56
73
|
```bash
|
|
57
|
-
publisher login --
|
|
74
|
+
publisher login --no-wait --json
|
|
58
75
|
```
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
It prints one JSON object with `userCode` and `verificationUri`, and `next.command`. Then:
|
|
78
|
+
|
|
79
|
+
1. **If a tool named `approve_publisher_sign_in` is available** (the Shadow connector, which the
|
|
80
|
+
person connected in Claude's settings), call it with `userCode`. Do not give them the link.
|
|
81
|
+
If the tool answers that it did not approve, do step 2 instead.
|
|
82
|
+
2. Otherwise give the person `verificationUri` as a link they can click, and `userCode`, and say
|
|
83
|
+
the page will ask them to sign in with their work account.
|
|
84
|
+
3. Run `next.command` — it is `publisher login --continue --json`. It returns `ok: true` within
|
|
85
|
+
seconds of the approval. If it returns `state: "pending"`, the code is not approved yet: wait
|
|
86
|
+
for the person, then run it again. It refuses after ten minutes; then start again.
|
|
87
|
+
4. **Check that the sign-in was kept before you publish**: run `publisher doctor --json` as a NEW
|
|
88
|
+
command. `next.action` must be `"ready"`. If it is not, or a command says the sign-in or the
|
|
89
|
+
waiting sign-in was not kept (its message names the file it looked for), this environment
|
|
90
|
+
forgot the file: keep it in another folder, as the section above says, and sign in again.
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
from the log, then keep reading the log as above — it reaches `ok: true` within a few seconds, with
|
|
69
|
-
nothing for the person to do. If the tool answers that it did not approve, give them the link and
|
|
70
|
-
code as above. The tool never returns a credential, and you never need one.
|
|
92
|
+
In a Claude Cowork task this happens again in every new task, because the sandbox forgets the
|
|
93
|
+
sign-in when the task ends. The tool never returns a credential, and you never need one.
|
|
71
94
|
|
|
72
95
|
**Never ask the user for a token, an endpoint, a URL or a credential — in any words.** They are
|
|
73
96
|
publishing an app; they do not know what those are, and you do not need them: `login` discovers
|
|
@@ -96,7 +119,10 @@ publisher sync . --message "<a few words on what changed>" --until-change --time
|
|
|
96
119
|
```
|
|
97
120
|
|
|
98
121
|
That one command records what changed in the folder, brings in anything published meanwhile (a
|
|
99
|
-
fast-forward or a clean merge — it never rewrites the user's work), and publishes the result.
|
|
122
|
+
fast-forward or a clean merge — it never rewrites the user's work), and publishes the result. Its
|
|
123
|
+
`timings` say how long each stage took (milliseconds): `discovery`, `authentication`, `link`,
|
|
124
|
+
`prepare`, `upload`, and — once the app is followed — `received`, `checks` and `deployment`. Quote
|
|
125
|
+
them when the person asks why it was slow. Write
|
|
100
126
|
the message in the user's words ("Add a contact page"), not git's.
|
|
101
127
|
|
|
102
128
|
A published app runs with no internet access: **it may not load anything from other sites** — no
|
package/dist/publisher.cjs
CHANGED
|
@@ -28,7 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
));
|
|
29
29
|
|
|
30
30
|
// src/main.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_path9 = require("node:path");
|
|
32
32
|
|
|
33
33
|
// ../protocol/src/versions.ts
|
|
34
34
|
var PROTOCOL_VERSION = 1;
|
|
@@ -2957,6 +2957,7 @@ var MAX_WAIT_SECONDS = 5;
|
|
|
2957
2957
|
var import_node_child_process = require("node:child_process");
|
|
2958
2958
|
var import_node_os = require("node:os");
|
|
2959
2959
|
var import_promises = require("node:fs/promises");
|
|
2960
|
+
var import_node_fs = require("node:fs");
|
|
2960
2961
|
var import_node_path = require("node:path");
|
|
2961
2962
|
var import_node_util = require("node:util");
|
|
2962
2963
|
var import_node_child_process2 = require("node:child_process");
|
|
@@ -2991,6 +2992,23 @@ var GIT_BIN = process.env["GITPUB_GIT_BIN"] ?? "git";
|
|
|
2991
2992
|
function neutralCwd() {
|
|
2992
2993
|
return (0, import_node_os.tmpdir)();
|
|
2993
2994
|
}
|
|
2995
|
+
function startFailure(label, error2, cwd) {
|
|
2996
|
+
const opts = { retryable: true, cause: error2 };
|
|
2997
|
+
if (error2.code !== "ENOENT") return new PublisherError("UNAVAILABLE", `${label} could not be started: ${error2.message}`, opts);
|
|
2998
|
+
if (!isDirectory(cwd)) {
|
|
2999
|
+
const which = cwd === (0, import_node_os.tmpdir)() ? "the temporary directory (TMPDIR)" : "its working directory";
|
|
3000
|
+
return new PublisherError("UNAVAILABLE", `${label} could not be started: ${which} ${cwd} does not exist`, { ...opts, details: { missing: "cwd", cwd } });
|
|
3001
|
+
}
|
|
3002
|
+
const where = GIT_BIN.includes("/") ? `at ${GIT_BIN}` : "on PATH";
|
|
3003
|
+
return new PublisherError("UNAVAILABLE", `${label} could not be started: git was not found ${where}`, { ...opts, details: { missing: "git", git: GIT_BIN } });
|
|
3004
|
+
}
|
|
3005
|
+
function isDirectory(path) {
|
|
3006
|
+
try {
|
|
3007
|
+
return (0, import_node_fs.statSync)(path).isDirectory();
|
|
3008
|
+
} catch {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2994
3012
|
var gitStats = { invocations: 0, totalMs: 0 };
|
|
2995
3013
|
var liveChildren = /* @__PURE__ */ new Set();
|
|
2996
3014
|
var closedChildren = /* @__PURE__ */ new WeakSet();
|
|
@@ -3158,11 +3176,12 @@ function spawnTracked(args, opts) {
|
|
|
3158
3176
|
if (typeof a !== "string") throw new Error("git args must be strings");
|
|
3159
3177
|
if (a.includes("\0")) throw new PublisherError("INVALID_REQUEST", "NUL in git argument");
|
|
3160
3178
|
}
|
|
3161
|
-
const
|
|
3179
|
+
const cwd = opts.cwd ?? neutralCwd();
|
|
3180
|
+
const child = spawnImpl(GIT_BIN, args, { cwd, env: sanitizedGitEnv(opts.env), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, detached: true });
|
|
3162
3181
|
gitStats.invocations++;
|
|
3163
3182
|
const cleanupBoundMs = opts.cleanupBoundMs ?? CLEANUP_BOUND_MS;
|
|
3164
3183
|
track(child, cleanupBoundMs);
|
|
3165
|
-
return { child, cleanupBoundMs };
|
|
3184
|
+
return { child, cleanupBoundMs, cwd };
|
|
3166
3185
|
}
|
|
3167
3186
|
function feedStdin(child, input, onSourceError) {
|
|
3168
3187
|
let unpipe = () => void 0;
|
|
@@ -3211,7 +3230,7 @@ async function runGit(args, opts = {}) {
|
|
|
3211
3230
|
const started = Date.now();
|
|
3212
3231
|
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
3213
3232
|
const maxOut = opts.maxStdoutBytes ?? 64 * 1024 * 1024;
|
|
3214
|
-
const { child, cleanupBoundMs } = spawnTracked(args, opts);
|
|
3233
|
+
const { child, cleanupBoundMs, cwd } = spawnTracked(args, opts);
|
|
3215
3234
|
child.once("exit", () => armCloseBound(child, cleanupBoundMs));
|
|
3216
3235
|
const stdoutChunks = [];
|
|
3217
3236
|
let outBytes = 0;
|
|
@@ -3261,7 +3280,7 @@ async function runGit(args, opts = {}) {
|
|
|
3261
3280
|
const result = { code, stdout: Buffer.concat(stdoutChunks), stderr, durationMs };
|
|
3262
3281
|
const cleanup = await reapChild(child, { boundMs: cleanupBoundMs, ...opts.groupObserver ? { observer: opts.groupObserver } : {} });
|
|
3263
3282
|
if (!reapSucceeded(cleanup)) throw cleanupFailure(`git ${args[0]}`, cleanup, killedReason ?? void 0);
|
|
3264
|
-
if (ended.error) throw
|
|
3283
|
+
if (ended.error) throw startFailure(`git ${args[0]}`, ended.error, cwd);
|
|
3265
3284
|
if (killedReason) throw killedError(killedReason, killedDetail, args, timeoutMs, maxOut);
|
|
3266
3285
|
if (code !== 0 && !opts.allowFailure) throw new GitCommandError(args, code, stderr, opts.label);
|
|
3267
3286
|
return result;
|
|
@@ -3334,8 +3353,8 @@ async function scanProcessGroup(pgid, signal) {
|
|
|
3334
3353
|
if (signal?.aborted) throw new Error("process-group scan abandoned by its caller");
|
|
3335
3354
|
if (!/^\d+$/.test(name)) continue;
|
|
3336
3355
|
try {
|
|
3337
|
-
const
|
|
3338
|
-
const rest =
|
|
3356
|
+
const stat4 = await (0, import_promises.readFile)(`/proc/${name}/stat`, "utf8");
|
|
3357
|
+
const rest = stat4.slice(stat4.lastIndexOf(")") + 2).split(" ");
|
|
3339
3358
|
if (Number(rest[2]) !== pgid) continue;
|
|
3340
3359
|
const status = await (0, import_promises.readFile)(`/proc/${name}/status`, "utf8");
|
|
3341
3360
|
members2.push({ pid: Number(name), rss: Number(/^VmRSS:\s+(\d+)/m.exec(status)?.[1] ?? 0) * 1024 });
|
|
@@ -3524,13 +3543,13 @@ function compareVersions(a, b) {
|
|
|
3524
3543
|
|
|
3525
3544
|
// ../git-core/src/hash.ts
|
|
3526
3545
|
var import_node_crypto2 = require("node:crypto");
|
|
3527
|
-
var
|
|
3546
|
+
var import_node_fs2 = require("node:fs");
|
|
3528
3547
|
var import_promises2 = require("node:fs/promises");
|
|
3529
3548
|
async function sha256File(path) {
|
|
3530
3549
|
const h = (0, import_node_crypto2.createHash)("sha256");
|
|
3531
3550
|
let size = 0;
|
|
3532
3551
|
await new Promise((resolve3, reject) => {
|
|
3533
|
-
(0,
|
|
3552
|
+
(0, import_node_fs2.createReadStream)(path).on("data", (c) => {
|
|
3534
3553
|
const b = typeof c === "string" ? Buffer.from(c) : c;
|
|
3535
3554
|
size += b.length;
|
|
3536
3555
|
h.update(b);
|
|
@@ -3542,12 +3561,12 @@ async function sha256File(path) {
|
|
|
3542
3561
|
}
|
|
3543
3562
|
|
|
3544
3563
|
// ../git-core/src/bare-repo.ts
|
|
3545
|
-
var
|
|
3564
|
+
var import_node_fs3 = require("node:fs");
|
|
3546
3565
|
async function indexPackFromFile(dir, packPath, opts = {}) {
|
|
3547
3566
|
const args = ["index-pack", "--stdin", ...opts.keep ?? true ? ["--keep=gitpub"] : []];
|
|
3548
3567
|
if (opts.strict ?? true) args.push("--strict", "--fsck-objects");
|
|
3549
3568
|
if (opts.maxInputBytes) args.push(`--max-input-size=${opts.maxInputBytes}`);
|
|
3550
|
-
const run = { cwd: dir, input: (0,
|
|
3569
|
+
const run = { cwd: dir, input: (0, import_node_fs3.createReadStream)(packPath), label: "index-pack", ...opts };
|
|
3551
3570
|
const res = await runGit(args, { ...run, allowFailure: true });
|
|
3552
3571
|
if (res.code !== 0) {
|
|
3553
3572
|
const msg = res.stderr.trim().split("\n").pop() ?? "index-pack failed";
|
|
@@ -3614,14 +3633,14 @@ function revParse(dir, rev) {
|
|
|
3614
3633
|
}
|
|
3615
3634
|
|
|
3616
3635
|
// ../git-core/src/pack.ts
|
|
3617
|
-
var
|
|
3636
|
+
var import_node_fs4 = require("node:fs");
|
|
3618
3637
|
async function buildPack(repoDir, opts) {
|
|
3619
3638
|
return packObjectsToFile(repoDir, ["--revs"], [...opts.include, ...opts.exclude.map((e) => `^${e}`)], opts);
|
|
3620
3639
|
}
|
|
3621
3640
|
async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
|
|
3622
3641
|
const args = ["pack-objects", ...extraArgs, "--stdout", "-q", "--delta-base-offset"];
|
|
3623
3642
|
const child = spawnGit(args, { cwd: repoDir, ...opts.cleanupBoundMs !== void 0 ? { cleanupBoundMs: opts.cleanupBoundMs } : {} });
|
|
3624
|
-
const out = (0,
|
|
3643
|
+
const out = (0, import_node_fs4.createWriteStream)(opts.outPath);
|
|
3625
3644
|
let stderr = "";
|
|
3626
3645
|
child.stderr.on("data", (c) => stderr += c.toString());
|
|
3627
3646
|
const timeoutMs = opts.timeoutMs ?? 6e5;
|
|
@@ -3652,7 +3671,7 @@ async function packObjectsToFile(repoDir, extraArgs, stdinLines, opts) {
|
|
|
3652
3671
|
}
|
|
3653
3672
|
}
|
|
3654
3673
|
const code = ended.code;
|
|
3655
|
-
if (ended.error) throw
|
|
3674
|
+
if (ended.error) throw startFailure("git pack-objects", ended.error, repoDir);
|
|
3656
3675
|
if (opts.signal?.aborted) throw new PublisherError("CANCELLED", "pack creation cancelled");
|
|
3657
3676
|
if (timedOut) throw new PublisherError("GIT", `pack-objects timed out after ${timeoutMs}ms`, { retryable: true });
|
|
3658
3677
|
if (code !== 0) throw new PublisherError("GIT", `pack-objects failed: ${stderr.trim().split("\n").pop() ?? ""}`);
|
|
@@ -4255,9 +4274,43 @@ async function postForm(base, path, fields, fetchImpl = globalThis.fetch) {
|
|
|
4255
4274
|
// ../client/src/credentials.ts
|
|
4256
4275
|
var REFRESH_EARLY_MS = 6e4;
|
|
4257
4276
|
function credentialsPath() {
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4277
|
+
return credentialsLocation().path;
|
|
4278
|
+
}
|
|
4279
|
+
function credentialsLocation() {
|
|
4280
|
+
const explicit = process.env["GIT_PUBLISHER_CREDENTIALS"];
|
|
4281
|
+
if (explicit) return { path: explicit, from: "GIT_PUBLISHER_CREDENTIALS" };
|
|
4282
|
+
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
4283
|
+
return { path: (0, import_node_path4.join)(xdg ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config"), "git-publisher", "credentials.json"), from: xdg ? "XDG_CONFIG_HOME" : "HOME" };
|
|
4284
|
+
}
|
|
4285
|
+
async function credentialsFileState() {
|
|
4286
|
+
let raw;
|
|
4287
|
+
try {
|
|
4288
|
+
raw = await (0, import_promises5.readFile)(credentialsPath(), "utf8");
|
|
4289
|
+
} catch (e) {
|
|
4290
|
+
return e.code === "ENOENT" ? "absent" : "unreadable";
|
|
4291
|
+
}
|
|
4292
|
+
try {
|
|
4293
|
+
JSON.parse(raw);
|
|
4294
|
+
return "present";
|
|
4295
|
+
} catch {
|
|
4296
|
+
return "unparseable";
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
async function whereCredentialsWere() {
|
|
4300
|
+
const { path, from } = credentialsLocation();
|
|
4301
|
+
const state = await credentialsFileState();
|
|
4302
|
+
const set = from === "HOME" ? "the default under HOME" : `set by ${from}`;
|
|
4303
|
+
const kept = state === "absent" ? ` ${notKept('"publisher login" answered ok', "sign in again")}` : "";
|
|
4304
|
+
return { sentence: `${WHAT_IS_THERE[state]} ${path} (${set}).${kept}`, details: { path, from, state } };
|
|
4305
|
+
}
|
|
4306
|
+
var WHAT_IS_THERE = {
|
|
4307
|
+
absent: "there is no credentials file at",
|
|
4308
|
+
present: "no connection is saved in",
|
|
4309
|
+
unreadable: "the credentials file cannot be read at",
|
|
4310
|
+
unparseable: "the credentials file cannot be parsed at"
|
|
4311
|
+
};
|
|
4312
|
+
function notKept(earlier, then) {
|
|
4313
|
+
return `If ${earlier} in an earlier command, this environment did not keep that file: set GIT_PUBLISHER_CREDENTIALS to a file in a folder that is kept between commands, outside the app folder, and ${then}.`;
|
|
4261
4314
|
}
|
|
4262
4315
|
function normalize(endpoint) {
|
|
4263
4316
|
return endpoint.replace(/\/+$/, "");
|
|
@@ -4305,11 +4358,13 @@ async function readCredentials() {
|
|
|
4305
4358
|
}
|
|
4306
4359
|
var writes = 0;
|
|
4307
4360
|
async function writeCredentials(file) {
|
|
4308
|
-
|
|
4361
|
+
return writeSecretJson(credentialsPath(), file);
|
|
4362
|
+
}
|
|
4363
|
+
async function writeSecretJson(path, value) {
|
|
4309
4364
|
await (0, import_promises5.mkdir)((0, import_node_path4.dirname)(path), { recursive: true, mode: 448 });
|
|
4310
4365
|
const tmp = `${path}.${process.pid}.${++writes}.tmp`;
|
|
4311
4366
|
try {
|
|
4312
|
-
await (0, import_promises5.writeFile)(tmp, JSON.stringify(
|
|
4367
|
+
await (0, import_promises5.writeFile)(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
4313
4368
|
await (0, import_promises5.chmod)(tmp, 384);
|
|
4314
4369
|
await (0, import_promises5.rename)(tmp, path);
|
|
4315
4370
|
} catch (e) {
|
|
@@ -4458,7 +4513,12 @@ async function resolveToken(endpoint, explicit, opts = {}) {
|
|
|
4458
4513
|
if (given) return given;
|
|
4459
4514
|
const url2 = normalize(endpoint);
|
|
4460
4515
|
const entry = (await readCredentials()).endpoints[url2];
|
|
4461
|
-
if (!entry)
|
|
4516
|
+
if (!entry) {
|
|
4517
|
+
const where = await whereCredentialsWere();
|
|
4518
|
+
throw new PublisherError("UNAUTHORIZED", `no credentials for ${endpoint}: ${where.sentence} Run "publisher login" or set GIT_PUBLISHER_TOKEN.`, {
|
|
4519
|
+
details: { signIn: "missing", credentialsFile: where.details }
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4462
4522
|
const now = (opts.now ?? Date.now)();
|
|
4463
4523
|
const lapsed = entry.expiresAt !== void 0 && Date.parse(entry.expiresAt) - REFRESH_EARLY_MS <= now;
|
|
4464
4524
|
if (entry.refreshToken && entry.authEndpoint && lapsed) return refreshCredential(url2, entry, opts);
|
|
@@ -5104,7 +5164,7 @@ async function checkIgnore(dir, paths) {
|
|
|
5104
5164
|
}
|
|
5105
5165
|
|
|
5106
5166
|
// ../client/src/fetch.ts
|
|
5107
|
-
var
|
|
5167
|
+
var import_node_fs6 = require("node:fs");
|
|
5108
5168
|
var import_promises8 = require("node:fs/promises");
|
|
5109
5169
|
var import_node_os3 = require("node:os");
|
|
5110
5170
|
var import_node_path6 = require("node:path");
|
|
@@ -5116,12 +5176,12 @@ var import_node_crypto4 = require("node:crypto");
|
|
|
5116
5176
|
var import_promises7 = require("node:fs/promises");
|
|
5117
5177
|
|
|
5118
5178
|
// ../client/src/upload.ts
|
|
5119
|
-
var
|
|
5179
|
+
var import_node_fs5 = require("node:fs");
|
|
5120
5180
|
var import_node_stream = require("node:stream");
|
|
5121
5181
|
async function uploadPack(fetchImpl, upload, packPath, size, opts) {
|
|
5122
5182
|
if (new Date(upload.expiresAt).getTime() < Date.now() + 5e3) return "expired";
|
|
5123
5183
|
let sent = 0;
|
|
5124
|
-
const file = (0,
|
|
5184
|
+
const file = (0, import_node_fs5.createReadStream)(packPath);
|
|
5125
5185
|
const progress = new TransformStream({
|
|
5126
5186
|
transform(chunk, controller) {
|
|
5127
5187
|
sent += chunk.byteLength;
|
|
@@ -5526,7 +5586,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call2) {
|
|
|
5526
5586
|
controller.enqueue(chunk);
|
|
5527
5587
|
}
|
|
5528
5588
|
});
|
|
5529
|
-
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0,
|
|
5589
|
+
await (0, import_promises9.pipeline)(import_node_stream2.Readable.fromWeb(res.body.pipeThrough(counted)), (0, import_node_fs6.createWriteStream)(dest));
|
|
5530
5590
|
if (n !== size || hash.digest("hex") !== sha256) throw new PublisherError("NETWORK", "downloaded artifact failed checksum verification", { retryable: true });
|
|
5531
5591
|
return n;
|
|
5532
5592
|
}
|
|
@@ -5829,10 +5889,8 @@ async function migrateAssociation(opts) {
|
|
|
5829
5889
|
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
5830
5890
|
var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
5831
5891
|
var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : fallback;
|
|
5832
|
-
async function
|
|
5892
|
+
async function startDeviceLogin(opts) {
|
|
5833
5893
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
5834
|
-
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5835
|
-
const now = opts.now ?? Date.now;
|
|
5836
5894
|
const control = normalize(opts.endpoint);
|
|
5837
5895
|
let advertised;
|
|
5838
5896
|
try {
|
|
@@ -5843,8 +5901,7 @@ async function deviceLogin(opts) {
|
|
|
5843
5901
|
if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
|
|
5844
5902
|
const base = normalize(advertised);
|
|
5845
5903
|
assertCredentialEndpoint(base);
|
|
5846
|
-
const
|
|
5847
|
-
const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
|
|
5904
|
+
const started = await postForm(base, "/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" }, doFetch);
|
|
5848
5905
|
if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
|
|
5849
5906
|
const s = started.body;
|
|
5850
5907
|
const deviceCode = str(s.device_code);
|
|
@@ -5852,17 +5909,33 @@ async function deviceLogin(opts) {
|
|
|
5852
5909
|
const uri = str(s.verification_uri_complete) ?? str(s.verification_uri);
|
|
5853
5910
|
if (!deviceCode || !userCode || !uri) throw new PublisherError("UNAVAILABLE", "the login response was incomplete");
|
|
5854
5911
|
const expiresInSeconds = num(s.expires_in, 600);
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5912
|
+
return {
|
|
5913
|
+
deviceCode,
|
|
5914
|
+
userCode,
|
|
5915
|
+
verificationUri: uri,
|
|
5916
|
+
expiresInSeconds,
|
|
5917
|
+
intervalSeconds: num(s.interval, 5),
|
|
5918
|
+
authEndpoint: base,
|
|
5919
|
+
expiresAt: (opts.now ?? Date.now)() + expiresInSeconds * 1e3
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
async function pollDeviceLogin(started, opts) {
|
|
5923
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
5924
|
+
const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5925
|
+
const now = opts.now ?? Date.now;
|
|
5926
|
+
let intervalMs = started.intervalSeconds * 1e3;
|
|
5927
|
+
let last = opts.lastPolledAt;
|
|
5858
5928
|
for (; ; ) {
|
|
5859
|
-
|
|
5860
|
-
if (
|
|
5861
|
-
|
|
5929
|
+
const at = Math.max(now(), last === null ? 0 : last + intervalMs);
|
|
5930
|
+
if (opts.until !== void 0 && at > opts.until) return null;
|
|
5931
|
+
if (at > now()) await sleep2(at - now());
|
|
5932
|
+
if (now() >= started.expiresAt) throw new PublisherError("EXPIRED", "the login expired before it was approved");
|
|
5933
|
+
const polled = await postForm(started.authEndpoint, "/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: started.deviceCode }, doFetch);
|
|
5934
|
+
last = now();
|
|
5862
5935
|
if (polled.status === 200) {
|
|
5863
5936
|
const token = str(polled.body["access_token"]);
|
|
5864
5937
|
if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
|
|
5865
|
-
return { token, refreshToken: str(polled.body["refresh_token"]), expiresInSeconds: num(polled.body["expires_in"], 900), authEndpoint:
|
|
5938
|
+
return { token, refreshToken: str(polled.body["refresh_token"]), expiresInSeconds: num(polled.body["expires_in"], 900), authEndpoint: started.authEndpoint };
|
|
5866
5939
|
}
|
|
5867
5940
|
const error2 = str(polled.body["error"]);
|
|
5868
5941
|
if (error2 === "authorization_pending") continue;
|
|
@@ -5876,33 +5949,44 @@ async function deviceLogin(opts) {
|
|
|
5876
5949
|
throw new PublisherError("UNAVAILABLE", `the login failed (${polled.status}${error2 ? `: ${error2}` : ""})`);
|
|
5877
5950
|
}
|
|
5878
5951
|
}
|
|
5952
|
+
async function deviceLogin(opts) {
|
|
5953
|
+
const started = await startDeviceLogin(opts);
|
|
5954
|
+
const { deviceCode: _secret, authEndpoint: _auth, expiresAt: _at, ...shown2 } = started;
|
|
5955
|
+
await opts.onPrompt(shown2);
|
|
5956
|
+
const signIn = await pollDeviceLogin(started, { ...opts, lastPolledAt: (opts.now ?? Date.now)() });
|
|
5957
|
+
return signIn;
|
|
5958
|
+
}
|
|
5879
5959
|
|
|
5880
|
-
// src/
|
|
5881
|
-
var
|
|
5882
|
-
|
|
5883
|
-
|
|
5960
|
+
// ../client/src/pending-login.ts
|
|
5961
|
+
var import_promises10 = require("node:fs/promises");
|
|
5962
|
+
var import_node_path7 = require("node:path");
|
|
5963
|
+
function pendingLoginPath() {
|
|
5964
|
+
return (0, import_node_path7.join)((0, import_node_path7.dirname)(credentialsPath()), "login-pending.json");
|
|
5884
5965
|
}
|
|
5885
|
-
async function
|
|
5886
|
-
|
|
5887
|
-
|
|
5966
|
+
async function savePendingLogin(pending) {
|
|
5967
|
+
return writeSecretJson(pendingLoginPath(), { version: 1, ...pending });
|
|
5968
|
+
}
|
|
5969
|
+
async function readPendingLogin() {
|
|
5970
|
+
let raw;
|
|
5888
5971
|
try {
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
}
|
|
5893
|
-
body = await response.json();
|
|
5894
|
-
} catch (e) {
|
|
5895
|
-
if (PublisherError.isPublisherError(e)) throw e;
|
|
5896
|
-
throw new PublisherError("NETWORK", `could not reach ${portal}: ${e instanceof Error ? e.message : String(e)}`, { retryable: true });
|
|
5897
|
-
}
|
|
5898
|
-
const endpoint = body?.publishingEndpoint;
|
|
5899
|
-
if (endpoint === null || endpoint === void 0) {
|
|
5900
|
-
throw new PublisherError("LOCAL_STATE", `${portal} is not set up for publishing; ask whoever runs it, or pass --endpoint`);
|
|
5901
|
-
}
|
|
5902
|
-
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
5903
|
-
throw new PublisherError("UNAVAILABLE", `${portal} answered with something this version does not understand`, { retryable: false });
|
|
5972
|
+
raw = JSON.parse(await (0, import_promises10.readFile)(pendingLoginPath(), "utf8"));
|
|
5973
|
+
} catch {
|
|
5974
|
+
return null;
|
|
5904
5975
|
}
|
|
5905
|
-
return
|
|
5976
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
5977
|
+
const r = raw;
|
|
5978
|
+
const s = r.started;
|
|
5979
|
+
if (r.version !== 1 || typeof r.endpoint !== "string" || !s) return null;
|
|
5980
|
+
if (typeof s.deviceCode !== "string" || typeof s.authEndpoint !== "string" || typeof s.expiresAt !== "number" || typeof s.intervalSeconds !== "number") return null;
|
|
5981
|
+
return {
|
|
5982
|
+
endpoint: r.endpoint,
|
|
5983
|
+
started: s,
|
|
5984
|
+
...typeof r.profile === "string" ? { profile: r.profile } : {},
|
|
5985
|
+
...r.setDefault === true ? { setDefault: true } : {}
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
async function removePendingLogin() {
|
|
5989
|
+
await (0, import_promises10.rm)(pendingLoginPath(), { force: true });
|
|
5906
5990
|
}
|
|
5907
5991
|
|
|
5908
5992
|
// src/git-minimum.ts
|
|
@@ -6132,15 +6216,81 @@ async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
|
6132
6216
|
return { repoId: made.repository.repoId, appId: made.appId };
|
|
6133
6217
|
}
|
|
6134
6218
|
|
|
6135
|
-
// src/
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6219
|
+
// src/args.ts
|
|
6220
|
+
var import_node_util2 = require("node:util");
|
|
6221
|
+
var OPTIONS = {
|
|
6222
|
+
endpoint: { type: "string" },
|
|
6223
|
+
profile: { type: "string" },
|
|
6224
|
+
token: { type: "string" },
|
|
6225
|
+
portal: { type: "string" },
|
|
6226
|
+
name: { type: "string" },
|
|
6227
|
+
repo: { type: "string" },
|
|
6228
|
+
message: { type: "string" },
|
|
6229
|
+
publication: { type: "string" },
|
|
6230
|
+
timeout: { type: "string" },
|
|
6231
|
+
branch: { type: "string" },
|
|
6232
|
+
ours: { type: "string" },
|
|
6233
|
+
theirs: { type: "string" },
|
|
6234
|
+
to: { type: "string" },
|
|
6235
|
+
cursor: { type: "string" },
|
|
6236
|
+
ref: { type: "string", multiple: true },
|
|
6237
|
+
json: { type: "boolean" },
|
|
6238
|
+
quiet: { type: "boolean" },
|
|
6239
|
+
force: { type: "boolean" },
|
|
6240
|
+
apply: { type: "boolean" },
|
|
6241
|
+
"mark-conflicts": { type: "boolean" },
|
|
6242
|
+
help: { type: "boolean" },
|
|
6243
|
+
version: { type: "boolean" },
|
|
6244
|
+
"set-default": { type: "boolean" },
|
|
6245
|
+
archived: { type: "boolean" },
|
|
6246
|
+
// A flag of its own, not a negation of --wait: there is no --wait.
|
|
6247
|
+
"no-wait": { type: "boolean" },
|
|
6248
|
+
"until-change": { type: "boolean" },
|
|
6249
|
+
// `login --continue`: finish the sign-in a `login --no-wait` started.
|
|
6250
|
+
continue: { type: "boolean" }
|
|
6251
|
+
};
|
|
6252
|
+
function str2(v) {
|
|
6253
|
+
return typeof v === "string" ? v : void 0;
|
|
6254
|
+
}
|
|
6255
|
+
function parseArgs(argv) {
|
|
6256
|
+
try {
|
|
6257
|
+
const { positionals, values } = (0, import_node_util2.parseArgs)({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
6258
|
+
return { cmd: positionals, flags: { ...values } };
|
|
6259
|
+
} catch (e) {
|
|
6260
|
+
throw new PublisherError("INVALID_REQUEST", e instanceof Error ? e.message : String(e));
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
// src/discover.ts
|
|
6265
|
+
var DEFAULT_PORTAL_URL = "https://shadow.minnovate.ai";
|
|
6266
|
+
function portalBase(env, flag) {
|
|
6267
|
+
return (flag ?? env["GIT_PUBLISHER_PORTAL"] ?? DEFAULT_PORTAL_URL).replace(/\/+$/, "");
|
|
6268
|
+
}
|
|
6269
|
+
async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
6270
|
+
const url2 = `${portal}/api/v1/config`;
|
|
6271
|
+
let body;
|
|
6272
|
+
try {
|
|
6273
|
+
const response = await fetchImpl(url2);
|
|
6274
|
+
if (!response.ok) {
|
|
6275
|
+
throw new PublisherError("UNAVAILABLE", `could not ask ${portal} where to publish (it answered ${response.status})`, { retryable: true });
|
|
6276
|
+
}
|
|
6277
|
+
body = await response.json();
|
|
6278
|
+
} catch (e) {
|
|
6279
|
+
if (PublisherError.isPublisherError(e)) throw e;
|
|
6280
|
+
throw new PublisherError("NETWORK", `could not reach ${portal}: ${e instanceof Error ? e.message : String(e)}`, { retryable: true });
|
|
6281
|
+
}
|
|
6282
|
+
const endpoint = body?.publishingEndpoint;
|
|
6283
|
+
if (endpoint === null || endpoint === void 0) {
|
|
6284
|
+
throw new PublisherError("LOCAL_STATE", `${portal} is not set up for publishing; ask whoever runs it, or pass --endpoint`);
|
|
6285
|
+
}
|
|
6286
|
+
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
6287
|
+
throw new PublisherError("UNAVAILABLE", `${portal} answered with something this version does not understand`, { retryable: false });
|
|
6288
|
+
}
|
|
6289
|
+
return endpoint;
|
|
6140
6290
|
}
|
|
6141
6291
|
|
|
6142
6292
|
// src/prompt.ts
|
|
6143
|
-
var
|
|
6293
|
+
var import_promises11 = require("node:readline/promises");
|
|
6144
6294
|
function isInteractive(env) {
|
|
6145
6295
|
return env.isTTY === true && !env.json;
|
|
6146
6296
|
}
|
|
@@ -6157,7 +6307,7 @@ function resolveChoice(answer, names) {
|
|
|
6157
6307
|
return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
|
|
6158
6308
|
}
|
|
6159
6309
|
async function ask3(question) {
|
|
6160
|
-
const rl = (0,
|
|
6310
|
+
const rl = (0, import_promises11.createInterface)({ input: process.stdin, output: process.stderr });
|
|
6161
6311
|
try {
|
|
6162
6312
|
return await rl.question(question);
|
|
6163
6313
|
} catch {
|
|
@@ -6202,58 +6352,205 @@ Profile name: `);
|
|
|
6202
6352
|
return null;
|
|
6203
6353
|
}
|
|
6204
6354
|
|
|
6205
|
-
// src/
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
}
|
|
6236
|
-
|
|
6355
|
+
// src/timings.ts
|
|
6356
|
+
function stopwatch(now = Date.now) {
|
|
6357
|
+
const started = now();
|
|
6358
|
+
let last = started;
|
|
6359
|
+
const laps = {};
|
|
6360
|
+
return {
|
|
6361
|
+
/** `reached` false records a stage this command did not go through: null, and no time taken. */
|
|
6362
|
+
lap(name, reached = true) {
|
|
6363
|
+
const t = now();
|
|
6364
|
+
if (!reached) {
|
|
6365
|
+
laps[name] ??= null;
|
|
6366
|
+
return t;
|
|
6367
|
+
}
|
|
6368
|
+
laps[name] = (laps[name] ?? 0) + (t - last);
|
|
6369
|
+
last = t;
|
|
6370
|
+
return t;
|
|
6371
|
+
},
|
|
6372
|
+
/** Every lap so far, with `total` since the stopwatch started. */
|
|
6373
|
+
read(extra = {}) {
|
|
6374
|
+
return { ...laps, ...extra, total: now() - started };
|
|
6375
|
+
}
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6378
|
+
function splitAtBegin(clock, onProgress) {
|
|
6379
|
+
let sending = false;
|
|
6380
|
+
return {
|
|
6381
|
+
onProgress(e) {
|
|
6382
|
+
if (!sending && e.phase === "begin") {
|
|
6383
|
+
sending = true;
|
|
6384
|
+
clock.lap("prepare");
|
|
6385
|
+
}
|
|
6386
|
+
onProgress(e);
|
|
6387
|
+
},
|
|
6388
|
+
done: () => sending ? clock.lap("upload") : (clock.lap("prepare"), null)
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
function between(from, to) {
|
|
6392
|
+
const a = from?.at ? Date.parse(from.at) : NaN;
|
|
6393
|
+
const b = to?.at ? Date.parse(to.at) : NaN;
|
|
6394
|
+
return Number.isFinite(a) && Number.isFinite(b) && to.state !== "skipped" ? Math.max(0, b - a) : null;
|
|
6395
|
+
}
|
|
6396
|
+
function runTimings(stages, acceptedAt) {
|
|
6397
|
+
const s = (name) => stages?.find((e) => e.stage === name);
|
|
6398
|
+
const receivedAt = s("received")?.at ? Date.parse(s("received").at) : NaN;
|
|
6399
|
+
return {
|
|
6400
|
+
handoff: acceptedAt !== null && Number.isFinite(receivedAt) ? Math.max(0, receivedAt - acceptedAt) : null,
|
|
6401
|
+
received: between(s("received"), s("running")),
|
|
6402
|
+
checks: between(s("running"), s("deploying")),
|
|
6403
|
+
deployment: between(s("deploying"), s("live"))
|
|
6404
|
+
};
|
|
6405
|
+
}
|
|
6406
|
+
function duration3(ms) {
|
|
6407
|
+
return ms < 1e4 ? `${(Math.max(0, ms) / 1e3).toFixed(1)} s` : took(ms);
|
|
6408
|
+
}
|
|
6409
|
+
function timingsLine(t) {
|
|
6410
|
+
const parts = Object.entries(t).filter((e) => e[1] !== null).map(([name, ms]) => `${name} ${duration3(ms)}`);
|
|
6411
|
+
return `Took: ${parts.join(" \xB7 ")}`;
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
// src/login.ts
|
|
6415
|
+
async function verifiedCapabilities(publisher) {
|
|
6416
|
+
const caps = await publisher.getCapabilities();
|
|
6417
|
+
await publisher.listRepositories({ limit: 1 });
|
|
6418
|
+
return caps;
|
|
6419
|
+
}
|
|
6420
|
+
var CONTINUE = "publisher login --continue --json";
|
|
6421
|
+
var START_AGAIN = "publisher login --no-wait --json";
|
|
6422
|
+
function prompt({ verificationUri, userCode, expiresInSeconds }) {
|
|
6423
|
+
process.stderr.write(
|
|
6424
|
+
`
|
|
6425
|
+
To sign in, open:
|
|
6426
|
+
${verificationUri}
|
|
6427
|
+
|
|
6428
|
+
and confirm the code:
|
|
6429
|
+
${userCode}
|
|
6430
|
+
|
|
6431
|
+
Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
6432
|
+
`
|
|
6433
|
+
);
|
|
6434
|
+
}
|
|
6435
|
+
async function login(ctx) {
|
|
6436
|
+
if (ctx.flags["continue"] === true) return continueLogin(ctx);
|
|
6437
|
+
const clock = stopwatch();
|
|
6438
|
+
const target = resolveLoginEndpoint(ctx.input);
|
|
6439
|
+
const endpoint = "endpoint" in target ? target.endpoint : await discoverEndpoint(portalBase(process.env, str2(ctx.flags["portal"])));
|
|
6440
|
+
if (!endpoint) throw new PublisherError("INVALID_REQUEST", "login requires --endpoint (or --profile naming a stored one)");
|
|
6441
|
+
clock.lap("discovery");
|
|
6442
|
+
const profile = str2(ctx.flags["profile"]);
|
|
6443
|
+
const setDefault = ctx.flags["set-default"] === true;
|
|
6444
|
+
const given = str2(ctx.flags["token"]) ?? process.env["GIT_PUBLISHER_TOKEN"];
|
|
6445
|
+
if (given) return finish(ctx, { endpoint, token: given, signIn: null, profile, setDefault }, clock);
|
|
6446
|
+
if (ctx.flags["no-wait"] === true) {
|
|
6447
|
+
const started = await startDeviceLogin({ endpoint });
|
|
6448
|
+
clock.lap("start");
|
|
6449
|
+
prompt(started);
|
|
6450
|
+
const pendingFile = await savePendingLogin({ endpoint, started, ...profile ? { profile } : {}, ...setDefault ? { setDefault } : {} });
|
|
6451
|
+
ctx.out(pendingResult(endpoint, started, "Have the code approved, then run next.command to finish signing in.", clock, { pendingFile }));
|
|
6452
|
+
return 0;
|
|
6453
|
+
}
|
|
6454
|
+
const signIn = await deviceLogin({
|
|
6455
|
+
endpoint,
|
|
6456
|
+
onPrompt: (shown2) => {
|
|
6457
|
+
clock.lap("start");
|
|
6458
|
+
prompt(shown2);
|
|
6459
|
+
}
|
|
6460
|
+
});
|
|
6461
|
+
clock.lap("approval");
|
|
6462
|
+
return finish(ctx, { endpoint, token: signIn.token, signIn, profile, setDefault }, clock);
|
|
6463
|
+
}
|
|
6464
|
+
async function continueLogin(ctx) {
|
|
6465
|
+
const clock = stopwatch();
|
|
6466
|
+
const pending = await readPendingLogin();
|
|
6467
|
+
if (pending === null) {
|
|
6468
|
+
const pendingFile = pendingLoginPath();
|
|
6469
|
+
throw new PublisherError("LOCAL_STATE", `no sign-in is waiting at ${pendingFile}. ${notKept(`"${START_AGAIN}" answered`, `run ${START_AGAIN} again`)}`, {
|
|
6470
|
+
details: { pendingFile, credentialsFile: credentialsLocation(), next: { action: "sign-in", ask: null, command: START_AGAIN, profiles: [] } }
|
|
6471
|
+
});
|
|
6472
|
+
}
|
|
6473
|
+
const timeout = str2(ctx.flags["timeout"]);
|
|
6474
|
+
let signIn;
|
|
6237
6475
|
try {
|
|
6238
|
-
|
|
6239
|
-
return { cmd: positionals, flags: { ...values } };
|
|
6476
|
+
signIn = await pollDeviceLogin(pending.started, { lastPolledAt: null, ...timeout === void 0 ? {} : { until: Date.now() + followTimeoutMs(timeout) } });
|
|
6240
6477
|
} catch (e) {
|
|
6241
|
-
|
|
6478
|
+
if (e instanceof PublisherError && (e.code === "EXPIRED" || e.code === "FORBIDDEN")) await removePendingLogin();
|
|
6479
|
+
throw e;
|
|
6242
6480
|
}
|
|
6481
|
+
clock.lap("approval");
|
|
6482
|
+
if (signIn === null) {
|
|
6483
|
+
ctx.out(pendingResult(pending.endpoint, pending.started, "The code is not approved yet. Have it approved, then run next.command again.", clock));
|
|
6484
|
+
return 3;
|
|
6485
|
+
}
|
|
6486
|
+
const code = await finish(
|
|
6487
|
+
ctx,
|
|
6488
|
+
{ endpoint: pending.endpoint, token: signIn.token, signIn, profile: pending.profile ?? str2(ctx.flags["profile"]), setDefault: pending.setDefault === true },
|
|
6489
|
+
clock
|
|
6490
|
+
);
|
|
6491
|
+
await removePendingLogin();
|
|
6492
|
+
return code;
|
|
6493
|
+
}
|
|
6494
|
+
function pendingResult(endpoint, started, hint, clock, extra = {}) {
|
|
6495
|
+
return {
|
|
6496
|
+
ok: false,
|
|
6497
|
+
state: "pending",
|
|
6498
|
+
endpoint,
|
|
6499
|
+
userCode: started.userCode,
|
|
6500
|
+
verificationUri: started.verificationUri,
|
|
6501
|
+
expiresAt: new Date(started.expiresAt).toISOString(),
|
|
6502
|
+
...extra,
|
|
6503
|
+
next: { action: "approve", ask: null, command: CONTINUE },
|
|
6504
|
+
hint,
|
|
6505
|
+
timings: clock.read()
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
async function finish(ctx, got, clock) {
|
|
6509
|
+
const { endpoint, token, signIn } = got;
|
|
6510
|
+
const caps = await verifiedCapabilities(createPublisher({ endpoint, token }));
|
|
6511
|
+
clock.lap("verify");
|
|
6512
|
+
let profile = got.profile;
|
|
6513
|
+
if (!profile && isInteractive({ json: ctx.json, isTTY: process.stdin.isTTY })) {
|
|
6514
|
+
const suggested = suggestProfileName(endpoint, new Set(Object.keys(ctx.input.file.profiles)));
|
|
6515
|
+
profile = await askProfileName(endpoint, suggested) ?? void 0;
|
|
6516
|
+
}
|
|
6517
|
+
const path = await saveCredential(endpoint, token, void 0, {
|
|
6518
|
+
...profile ? { profile } : {},
|
|
6519
|
+
setDefault: got.setDefault,
|
|
6520
|
+
// The refresh half, when the service issued one: the sign-in outlives the credential.
|
|
6521
|
+
...signIn ? { signIn } : {}
|
|
6522
|
+
});
|
|
6523
|
+
const file = await readCredentials();
|
|
6524
|
+
clock.lap("save");
|
|
6525
|
+
const named = profileForEndpoint(file, endpoint, profile);
|
|
6526
|
+
const timings = clock.read();
|
|
6527
|
+
ctx.log(timingsLine(timings));
|
|
6528
|
+
ctx.out({
|
|
6529
|
+
ok: true,
|
|
6530
|
+
endpoint,
|
|
6531
|
+
profile: named,
|
|
6532
|
+
isDefault: named !== null && file.defaultProfile === named,
|
|
6533
|
+
server: caps.server,
|
|
6534
|
+
credentialsFile: path,
|
|
6535
|
+
// Which setting chose that path: a sandbox that keeps only its workspace needs it to be there.
|
|
6536
|
+
credentialsFrom: credentialsLocation().from,
|
|
6537
|
+
timings
|
|
6538
|
+
});
|
|
6539
|
+
return 0;
|
|
6243
6540
|
}
|
|
6244
6541
|
|
|
6245
6542
|
// src/skills.ts
|
|
6246
6543
|
var import_node_child_process3 = require("node:child_process");
|
|
6247
|
-
var
|
|
6248
|
-
var
|
|
6544
|
+
var import_node_fs7 = require("node:fs");
|
|
6545
|
+
var import_node_path8 = require("node:path");
|
|
6249
6546
|
var MARKETPLACE = "shadow-publisher";
|
|
6250
6547
|
var PLUGIN = "git-publisher";
|
|
6251
6548
|
function packageRoot() {
|
|
6252
|
-
return (0,
|
|
6549
|
+
return (0, import_node_path8.join)((0, import_node_path8.dirname)((0, import_node_fs7.realpathSync)(process.argv[1])), "..");
|
|
6253
6550
|
}
|
|
6254
6551
|
function packageVersion() {
|
|
6255
6552
|
try {
|
|
6256
|
-
return JSON.parse((0,
|
|
6553
|
+
return JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path8.join)(packageRoot(), "package.json"), "utf8")).version;
|
|
6257
6554
|
} catch {
|
|
6258
6555
|
return "unknown";
|
|
6259
6556
|
}
|
|
@@ -6270,8 +6567,8 @@ function claude(args) {
|
|
|
6270
6567
|
});
|
|
6271
6568
|
}
|
|
6272
6569
|
async function installSkills({ log }) {
|
|
6273
|
-
const source = (0,
|
|
6274
|
-
if (!(0,
|
|
6570
|
+
const source = (0, import_node_path8.join)(packageRoot(), "dist", "claude");
|
|
6571
|
+
if (!(0, import_node_fs7.existsSync)((0, import_node_path8.join)(source, ".claude-plugin", "marketplace.json"))) {
|
|
6275
6572
|
throw new PublisherError("LOCAL_STATE", `this publisher carries no skills at ${source}; reinstall it with: npm install -g @minnovate/shadow-publisher`);
|
|
6276
6573
|
}
|
|
6277
6574
|
const step = async (what, args) => {
|
|
@@ -6369,6 +6666,46 @@ function hintFor(view) {
|
|
|
6369
6666
|
return `It is waiting for your answer. Run: publisher answer ${how}`;
|
|
6370
6667
|
}
|
|
6371
6668
|
|
|
6669
|
+
// src/tempdir.ts
|
|
6670
|
+
var import_node_fs8 = require("node:fs");
|
|
6671
|
+
var import_promises12 = require("node:fs/promises");
|
|
6672
|
+
var import_node_os4 = require("node:os");
|
|
6673
|
+
var FALLBACK_TEMP_DIRS = ["/tmp", "/var/tmp"];
|
|
6674
|
+
async function unusable(path) {
|
|
6675
|
+
try {
|
|
6676
|
+
if (!(await (0, import_promises12.stat)(path)).isDirectory()) return "is not a directory";
|
|
6677
|
+
} catch {
|
|
6678
|
+
return "does not exist";
|
|
6679
|
+
}
|
|
6680
|
+
try {
|
|
6681
|
+
await (0, import_promises12.access)(path, import_node_fs8.constants.W_OK | import_node_fs8.constants.X_OK);
|
|
6682
|
+
return null;
|
|
6683
|
+
} catch {
|
|
6684
|
+
return "cannot be written";
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
async function ensureTempDir(env = process.env, fallbacks = FALLBACK_TEMP_DIRS) {
|
|
6688
|
+
const named = (0, import_node_os4.tmpdir)();
|
|
6689
|
+
const reason = await unusable(named);
|
|
6690
|
+
if (reason === null) return { path: named, replaced: null };
|
|
6691
|
+
for (const candidate of fallbacks) {
|
|
6692
|
+
if (candidate !== named && await unusable(candidate) === null) {
|
|
6693
|
+
env["TMPDIR"] = candidate;
|
|
6694
|
+
return { path: candidate, replaced: { path: named, reason } };
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
throw new PublisherError(
|
|
6698
|
+
"LOCAL_STATE",
|
|
6699
|
+
`the temporary directory ${named} ${reason}, and neither ${fallbacks.join(" nor ")} can be used: set TMPDIR to a directory that can be written`,
|
|
6700
|
+
{
|
|
6701
|
+
details: { tempDir: named, reason, tried: [...fallbacks] }
|
|
6702
|
+
}
|
|
6703
|
+
);
|
|
6704
|
+
}
|
|
6705
|
+
function replacedLine(t) {
|
|
6706
|
+
return t.replaced === null ? null : `note: the temporary directory ${t.replaced.path} ${t.replaced.reason}; using ${t.path} instead`;
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6372
6709
|
// src/main.ts
|
|
6373
6710
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
6374
6711
|
|
|
@@ -6382,6 +6719,8 @@ Usage:
|
|
|
6382
6719
|
[--endpoint <url>] skip discovery and use this one
|
|
6383
6720
|
[--portal <url>] ask a different platform (or GIT_PUBLISHER_PORTAL)
|
|
6384
6721
|
[--profile <name>] [--set-default] [--token <token>]
|
|
6722
|
+
[--no-wait] print the code and exit; finish with login --continue
|
|
6723
|
+
publisher login --continue [--timeout <seconds>] collect the credential once the code is approved
|
|
6385
6724
|
publisher logout (--endpoint <url> | --profile <name>)
|
|
6386
6725
|
publisher profile list | profile use <name> | profile remove <name>
|
|
6387
6726
|
publisher init [dir] [--name <name>] [--repo <id>] create (or attach to) a repository, associate this folder
|
|
@@ -6438,9 +6777,6 @@ function optStr(flags, key) {
|
|
|
6438
6777
|
const v = str2(flags[key]);
|
|
6439
6778
|
return v ? { [key]: v } : {};
|
|
6440
6779
|
}
|
|
6441
|
-
function str2(v) {
|
|
6442
|
-
return typeof v === "string" ? v : void 0;
|
|
6443
|
-
}
|
|
6444
6780
|
async function main(argv) {
|
|
6445
6781
|
const { cmd, flags } = parseArgs(argv);
|
|
6446
6782
|
const json = flags["json"] === true;
|
|
@@ -6455,7 +6791,7 @@ async function main(argv) {
|
|
|
6455
6791
|
process.stderr.write(`\r[${e.phase}] ${e.message}${pct} `);
|
|
6456
6792
|
if (e.phase === "done") process.stderr.write("\n");
|
|
6457
6793
|
};
|
|
6458
|
-
const dirArg = (i) => (0,
|
|
6794
|
+
const dirArg = (i) => (0, import_node_path9.resolve)(cmd[i] ?? ".");
|
|
6459
6795
|
const following = { ...stageEmitter(json, quiet), log, untilChange: flags["until-change"] === true };
|
|
6460
6796
|
const resolutionInput = async (dir) => {
|
|
6461
6797
|
let association;
|
|
@@ -6483,7 +6819,11 @@ async function main(argv) {
|
|
|
6483
6819
|
const client = async (dir) => {
|
|
6484
6820
|
const { endpoint, source } = await endpointFor(dir);
|
|
6485
6821
|
const token = str2(flags["token"]);
|
|
6486
|
-
return {
|
|
6822
|
+
return {
|
|
6823
|
+
endpoint,
|
|
6824
|
+
source,
|
|
6825
|
+
p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION })
|
|
6826
|
+
};
|
|
6487
6827
|
};
|
|
6488
6828
|
const linkFolder = async (p, endpoint, dir, attach) => {
|
|
6489
6829
|
let onboarding = null;
|
|
@@ -6515,12 +6855,17 @@ async function main(argv) {
|
|
|
6515
6855
|
`);
|
|
6516
6856
|
return 0;
|
|
6517
6857
|
}
|
|
6858
|
+
if (flags["help"] !== true && GIT_COMMANDS.has(cmd[0] ?? "")) {
|
|
6859
|
+
const line = replacedLine(await ensureTempDir());
|
|
6860
|
+
if (line !== null) process.stderr.write(line + "\n");
|
|
6861
|
+
}
|
|
6518
6862
|
switch (flags["help"] === true ? "help" : cmd[0]) {
|
|
6519
6863
|
case void 0:
|
|
6520
6864
|
case "help":
|
|
6521
6865
|
process.stdout.write(USAGE);
|
|
6522
6866
|
return 0;
|
|
6523
6867
|
case "doctor": {
|
|
6868
|
+
const temp = await ensureTempDir().catch((e) => e instanceof PublisherError ? { error: e.message } : null);
|
|
6524
6869
|
const git = await gitVersion().catch(() => null);
|
|
6525
6870
|
const dir = dirArg(1);
|
|
6526
6871
|
const s = credentialStatus(await resolutionInput(dir));
|
|
@@ -6542,6 +6887,7 @@ async function main(argv) {
|
|
|
6542
6887
|
profiles: s.profiles,
|
|
6543
6888
|
defaultProfile: s.defaultProfile,
|
|
6544
6889
|
credentialsFile: credentialsPath(),
|
|
6890
|
+
tempDir: temp,
|
|
6545
6891
|
association: association === null ? null : { endpoint: association.endpoint, repoId: association.repoId },
|
|
6546
6892
|
// Printed as it comes: `moved` carries where to, `unreachable` carries why, and the two
|
|
6547
6893
|
// need different next steps so they are not collapsed into "not ok".
|
|
@@ -6561,48 +6907,8 @@ async function main(argv) {
|
|
|
6561
6907
|
out({ ...result, next: { action: "reauthenticate", ask: null, command } });
|
|
6562
6908
|
return 0;
|
|
6563
6909
|
}
|
|
6564
|
-
case "login":
|
|
6565
|
-
|
|
6566
|
-
const input = await resolutionInput();
|
|
6567
|
-
const target = resolveLoginEndpoint(input);
|
|
6568
|
-
const endpoint = "endpoint" in target ? target.endpoint : await discoverEndpoint(portal);
|
|
6569
|
-
if (!endpoint) throw new PublisherError("INVALID_REQUEST", "login requires --endpoint (or --profile naming a stored one)");
|
|
6570
|
-
const given = str2(flags["token"]) ?? process.env["GIT_PUBLISHER_TOKEN"];
|
|
6571
|
-
const signIn = given ? null : await deviceLogin({
|
|
6572
|
-
endpoint,
|
|
6573
|
-
onPrompt: ({ userCode, verificationUri, expiresInSeconds }) => {
|
|
6574
|
-
process.stderr.write(
|
|
6575
|
-
`
|
|
6576
|
-
To sign in, open:
|
|
6577
|
-
${verificationUri}
|
|
6578
|
-
|
|
6579
|
-
and confirm the code:
|
|
6580
|
-
${userCode}
|
|
6581
|
-
|
|
6582
|
-
Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
6583
|
-
`
|
|
6584
|
-
);
|
|
6585
|
-
}
|
|
6586
|
-
});
|
|
6587
|
-
const token = given ?? signIn.token;
|
|
6588
|
-
const p = createPublisher({ endpoint, token });
|
|
6589
|
-
const caps = await verifiedCapabilities(p);
|
|
6590
|
-
let profileFlag = str2(flags["profile"]);
|
|
6591
|
-
if (!profileFlag && isInteractive({ json, isTTY: process.stdin.isTTY })) {
|
|
6592
|
-
const suggested = suggestProfileName(endpoint, new Set(Object.keys(input.file.profiles)));
|
|
6593
|
-
profileFlag = await askProfileName(endpoint, suggested) ?? void 0;
|
|
6594
|
-
}
|
|
6595
|
-
const path = await saveCredential(endpoint, token, void 0, {
|
|
6596
|
-
...profileFlag ? { profile: profileFlag } : {},
|
|
6597
|
-
setDefault: flags["set-default"] === true,
|
|
6598
|
-
// The refresh half, when the service issued one: the sign-in outlives the credential.
|
|
6599
|
-
...signIn ? { signIn } : {}
|
|
6600
|
-
});
|
|
6601
|
-
const file = await readCredentials();
|
|
6602
|
-
const profile = profileForEndpoint(file, endpoint, profileFlag);
|
|
6603
|
-
out({ ok: true, endpoint, profile, isDefault: profile !== null && file.defaultProfile === profile, server: caps.server, credentialsFile: path });
|
|
6604
|
-
return 0;
|
|
6605
|
-
}
|
|
6910
|
+
case "login":
|
|
6911
|
+
return login({ flags, json, out, log, input: await resolutionInput() });
|
|
6606
6912
|
case "logout": {
|
|
6607
6913
|
const profile = str2(flags["profile"]);
|
|
6608
6914
|
const endpoint = str2(flags["endpoint"]) ?? process.env["GIT_PUBLISHER_ENDPOINT"];
|
|
@@ -6740,11 +7046,20 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6740
7046
|
case "sync": {
|
|
6741
7047
|
const dir = dirArg(1);
|
|
6742
7048
|
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6743
|
-
const
|
|
7049
|
+
const clock = stopwatch();
|
|
7050
|
+
const { p, endpoint, source } = await client(dir);
|
|
7051
|
+
clock.lap("discovery");
|
|
7052
|
+
await resolveToken(endpoint, str2(flags["token"]), { endpointSource: source });
|
|
7053
|
+
clock.lap("authentication");
|
|
6744
7054
|
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6745
|
-
|
|
7055
|
+
clock.lap("link", linked !== null);
|
|
7056
|
+
const sending = splitAtBegin(clock, progress);
|
|
7057
|
+
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: sending.onProgress });
|
|
7058
|
+
const acceptedAt = sending.done();
|
|
6746
7059
|
const o = r.published;
|
|
6747
7060
|
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { ...following, timeoutMs, dir }) : null;
|
|
7061
|
+
const syncTimings = clock.read(followed !== null && "stages" in followed ? runTimings(followed.stages, acceptedAt) : {});
|
|
7062
|
+
log(timingsLine(syncTimings));
|
|
6748
7063
|
out({
|
|
6749
7064
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6750
7065
|
state: r.state,
|
|
@@ -6762,7 +7077,8 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6762
7077
|
...o.next === null ? {} : { next: o.next }
|
|
6763
7078
|
},
|
|
6764
7079
|
hint: syncHint(r.state, r.reconciled.before.conflicts, r.marked),
|
|
6765
|
-
progress: followed
|
|
7080
|
+
progress: followed,
|
|
7081
|
+
timings: syncTimings
|
|
6766
7082
|
});
|
|
6767
7083
|
return r.state === "published" || r.state === "up_to_date" ? 0 : r.state === "unknown" ? 3 : 2;
|
|
6768
7084
|
}
|
|
@@ -6783,7 +7099,11 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6783
7099
|
if (publication.state !== "accepted") {
|
|
6784
7100
|
throw new PublisherError("LOCAL_STATE", `publication ${publicationId} is ${publication.state}, not accepted; run publisher status`);
|
|
6785
7101
|
}
|
|
6786
|
-
|
|
7102
|
+
const clock = stopwatch();
|
|
7103
|
+
const followed = await followPublication(p, endpoint, publication, { ...following, timeoutMs, dir });
|
|
7104
|
+
const timings = clock.read("stages" in followed ? runTimings(followed.stages, null) : {});
|
|
7105
|
+
log(timingsLine(timings));
|
|
7106
|
+
out({ publicationId, progress: followed, timings });
|
|
6787
7107
|
return 0;
|
|
6788
7108
|
}
|
|
6789
7109
|
/**
|
|
@@ -6800,7 +7120,7 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6800
7120
|
}
|
|
6801
7121
|
/** Answer it, as `question=value` pairs, re-reading the preparation first. */
|
|
6802
7122
|
case "answer": {
|
|
6803
|
-
const dir = (0,
|
|
7123
|
+
const dir = (0, import_node_path9.resolve)(".");
|
|
6804
7124
|
const { p, endpoint } = await client(dir);
|
|
6805
7125
|
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6806
7126
|
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
@@ -6843,7 +7163,8 @@ ${USAGE}`);
|
|
|
6843
7163
|
return 1;
|
|
6844
7164
|
}
|
|
6845
7165
|
}
|
|
6846
|
-
|
|
7166
|
+
var GIT_COMMANDS = /* @__PURE__ */ new Set(["init", "publish", "status", "fetch", "checkout", "workspace-status", "sync", "merge-check"]);
|
|
7167
|
+
async function demand(r, input) {
|
|
6847
7168
|
if (r.kind === "resolved") return r;
|
|
6848
7169
|
const next = nextStep(credentialStatus(input));
|
|
6849
7170
|
const details = { profiles: r.kind === "none" ? [] : r.profiles, next };
|
|
@@ -6853,8 +7174,10 @@ function demand(r, input) {
|
|
|
6853
7174
|
throw new PublisherError("LOCAL_STATE", `no such profile: ${r.name}.${run}`, { details });
|
|
6854
7175
|
case "ambiguous":
|
|
6855
7176
|
throw new PublisherError("LOCAL_STATE", `several connections and none is the default: pass --profile <name>, or choose one once.${run}`, { details });
|
|
6856
|
-
case "none":
|
|
6857
|
-
|
|
7177
|
+
case "none": {
|
|
7178
|
+
const where = await whereCredentialsWere();
|
|
7179
|
+
throw new PublisherError("LOCAL_STATE", `not connected to a publishing service yet: ${where.sentence}${run}`, { details: { ...details, credentialsFile: where.details } });
|
|
7180
|
+
}
|
|
6858
7181
|
}
|
|
6859
7182
|
}
|
|
6860
7183
|
function syncHint(state, conflicts, marked) {
|