@botiverse/hands-cli 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 +71 -0
- package/dist/commands/apps.d.ts +7 -0
- package/dist/commands/apps.js +71 -0
- package/dist/commands/apps.js.map +1 -0
- package/dist/commands/builds.d.ts +14 -0
- package/dist/commands/builds.js +611 -0
- package/dist/commands/builds.js.map +1 -0
- package/dist/commands/deploy_tokens.d.ts +12 -0
- package/dist/commands/deploy_tokens.js +98 -0
- package/dist/commands/deploy_tokens.js.map +1 -0
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +141 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +129 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/releases.d.ts +5 -0
- package/dist/commands/releases.js +200 -0
- package/dist/commands/releases.js.map +1 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.js +43 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +25 -0
- package/dist/lib/api.js +126 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.js +71 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/env.d.ts +7 -0
- package/dist/lib/env.js +10 -0
- package/dist/lib/env.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver deploy-tokens` — mint / list / revoke app-scoped deploy tokens.
|
|
3
|
+
*
|
|
4
|
+
* Wires GET/POST/DELETE /api/apps/:appId/deploy-tokens. Requires app admin
|
|
5
|
+
* (otherwise the API returns an admin-native error telling you who can grant
|
|
6
|
+
* the role). A created token is printed once — the server stores only a hash —
|
|
7
|
+
* so capture it immediately (e.g. into a CI secret). Deploy tokens authenticate
|
|
8
|
+
* against the Quiver API as `Authorization: Bearer <token>`, which the CLI also
|
|
9
|
+
* reads from the QUIVER_BEARER_TOKEN env var.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
export declare function registerDeployTokenCommands(program: Command): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver deploy-tokens` — mint / list / revoke app-scoped deploy tokens.
|
|
3
|
+
*
|
|
4
|
+
* Wires GET/POST/DELETE /api/apps/:appId/deploy-tokens. Requires app admin
|
|
5
|
+
* (otherwise the API returns an admin-native error telling you who can grant
|
|
6
|
+
* the role). A created token is printed once — the server stores only a hash —
|
|
7
|
+
* so capture it immediately (e.g. into a CI secret). Deploy tokens authenticate
|
|
8
|
+
* against the Quiver API as `Authorization: Bearer <token>`, which the CLI also
|
|
9
|
+
* reads from the QUIVER_BEARER_TOKEN env var.
|
|
10
|
+
*/
|
|
11
|
+
import { apiRequest } from "../lib/api.js";
|
|
12
|
+
async function resolveAppId(appIdOrSlug) {
|
|
13
|
+
const isUuid = appIdOrSlug.length === 36 && appIdOrSlug.split("-").length === 5;
|
|
14
|
+
if (isUuid)
|
|
15
|
+
return appIdOrSlug;
|
|
16
|
+
const res = await apiRequest("/api/apps");
|
|
17
|
+
const match = res.apps.find((a) => a.slug === appIdOrSlug);
|
|
18
|
+
if (!match) {
|
|
19
|
+
console.error(`No app with slug '${appIdOrSlug}'.`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return match.id;
|
|
23
|
+
}
|
|
24
|
+
export function registerDeployTokenCommands(program) {
|
|
25
|
+
const dt = program
|
|
26
|
+
.command("deploy-tokens")
|
|
27
|
+
.description("Mint, list, and revoke app-scoped deploy tokens (requires app admin).");
|
|
28
|
+
dt.command("list <appIdOrSlug>")
|
|
29
|
+
.alias("ls")
|
|
30
|
+
.description("List an app's deploy tokens (metadata only, no secret values).")
|
|
31
|
+
.option("--json", "Output JSON.", false)
|
|
32
|
+
.action(async (appIdOrSlug, opts) => {
|
|
33
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
34
|
+
const res = await apiRequest(`/api/apps/${appId}/deploy-tokens`);
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
console.log(JSON.stringify(res, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (res.deploy_tokens.length === 0) {
|
|
40
|
+
console.log("No deploy tokens.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(["NAME", "ROLE", "PREFIX", "EXPIRES", "REVOKED", "ID"].join("\t"));
|
|
44
|
+
for (const t of res.deploy_tokens) {
|
|
45
|
+
console.log([
|
|
46
|
+
t.name,
|
|
47
|
+
t.app_role,
|
|
48
|
+
t.token_prefix,
|
|
49
|
+
t.expires_at ? new Date(t.expires_at).toISOString() : "never",
|
|
50
|
+
t.revoked_at ? "yes" : "no",
|
|
51
|
+
t.id.slice(0, 8),
|
|
52
|
+
].join("\t"));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
dt.command("create <appIdOrSlug>")
|
|
56
|
+
.description("Mint a new deploy token. The token is printed once — capture it now.")
|
|
57
|
+
.requiredOption("--name <name>", "Human label for the token (e.g. github-ci).")
|
|
58
|
+
.option("--role <role>", "publisher | viewer", "publisher")
|
|
59
|
+
.option("--expires-in-days <days>", "Expiry in days from now (default: never expires).")
|
|
60
|
+
.option("--json", "Output JSON.", false)
|
|
61
|
+
.action(async (appIdOrSlug, opts) => {
|
|
62
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
63
|
+
let expiresAt = null;
|
|
64
|
+
if (opts.expiresInDays != null) {
|
|
65
|
+
const days = Number(opts.expiresInDays);
|
|
66
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
67
|
+
console.error("--expires-in-days must be a positive number.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
expiresAt = Date.now() + days * 24 * 60 * 60 * 1000;
|
|
71
|
+
}
|
|
72
|
+
const res = await apiRequest(`/api/apps/${appId}/deploy-tokens`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: { name: opts.name, app_role: opts.role, expires_at: expiresAt },
|
|
75
|
+
});
|
|
76
|
+
if (opts.json) {
|
|
77
|
+
console.log(JSON.stringify(res, null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const t = res.deploy_token;
|
|
81
|
+
console.log(`Deploy token '${t.name}' (${t.app_role}) created.`);
|
|
82
|
+
console.log(` id: ${t.id}`);
|
|
83
|
+
console.log(` expires: ${t.expires_at ? new Date(t.expires_at).toISOString() : "never"}`);
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(" token (shown once — store it now, e.g. as a CI secret):");
|
|
86
|
+
console.log(` ${res.token}`);
|
|
87
|
+
});
|
|
88
|
+
dt.command("revoke <appIdOrSlug> <tokenId>")
|
|
89
|
+
.description("Revoke a deploy token by id.")
|
|
90
|
+
.action(async (appIdOrSlug, tokenId) => {
|
|
91
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
92
|
+
await apiRequest(`/api/apps/${appId}/deploy-tokens/${tokenId}`, {
|
|
93
|
+
method: "DELETE",
|
|
94
|
+
});
|
|
95
|
+
console.log(`Revoked deploy token ${tokenId}.`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=deploy_tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy_tokens.js","sourceRoot":"","sources":["../../src/commands/deploy_tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver feedback` — agent-friendly ticket triage from the terminal.
|
|
3
|
+
* Works with app-scoped deploy tokens (viewer for list/show, publisher for
|
|
4
|
+
* update/comment).
|
|
5
|
+
*/
|
|
6
|
+
import type { Command } from "commander";
|
|
7
|
+
export declare function registerFeedbackCommands(program: Command): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { apiRequest } from "../lib/api.js";
|
|
3
|
+
async function resolveAppId(slugOrId) {
|
|
4
|
+
if (slugOrId.length === 36 && slugOrId.split("-").length === 5)
|
|
5
|
+
return slugOrId;
|
|
6
|
+
const res = await apiRequest("/api/apps");
|
|
7
|
+
const match = res.apps.find((a) => a.slug === slugOrId);
|
|
8
|
+
if (!match) {
|
|
9
|
+
console.error(`No app with slug '${slugOrId}'.`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return match.id;
|
|
13
|
+
}
|
|
14
|
+
export function registerFeedbackCommands(program) {
|
|
15
|
+
const feedback = program
|
|
16
|
+
.command("feedback")
|
|
17
|
+
.description("Triage feedback/crash tickets.");
|
|
18
|
+
feedback
|
|
19
|
+
.command("list <appIdOrSlug>")
|
|
20
|
+
.description("List tickets, newest first.")
|
|
21
|
+
.option("--status <status>", "Filter: open | in_progress | resolved | closed.")
|
|
22
|
+
.option("--kind <kind>", "Filter: feedback | bug | crash.")
|
|
23
|
+
.option("--json", "Output JSON.", false)
|
|
24
|
+
.action(async (appIdOrSlug, opts) => {
|
|
25
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
if (opts.status)
|
|
28
|
+
params.set("status", opts.status);
|
|
29
|
+
if (opts.kind)
|
|
30
|
+
params.set("kind", opts.kind);
|
|
31
|
+
const qs = params.toString();
|
|
32
|
+
const res = await apiRequest(`/api/apps/${appId}/feedback${qs ? `?${qs}` : ""}`);
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
console.log(JSON.stringify(res, null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (res.tickets.length === 0) {
|
|
38
|
+
console.log("No tickets.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const t of res.tickets) {
|
|
42
|
+
const preview = t.message.replace(/\s+/g, " ").slice(0, 60);
|
|
43
|
+
console.log(`${t.id} ${t.status.padEnd(11)} ${t.kind.padEnd(8)} ` +
|
|
44
|
+
`${(t.assignee ?? "-").padEnd(16)} v${t.version_name ?? "?"} ` +
|
|
45
|
+
`[${t.attachment_count}📎 ${t.comment_count}💬] ${preview}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
feedback
|
|
49
|
+
.command("show <appIdOrSlug> <ticketId>")
|
|
50
|
+
.description("Show a ticket with device context, attachments, and comments.")
|
|
51
|
+
.option("--json", "Output JSON.", false)
|
|
52
|
+
.action(async (appIdOrSlug, ticketId, opts) => {
|
|
53
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
54
|
+
const res = await apiRequest(`/api/apps/${appId}/feedback/${ticketId}`);
|
|
55
|
+
if (opts.json) {
|
|
56
|
+
console.log(JSON.stringify(res, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const t = res.ticket;
|
|
60
|
+
console.log(`Ticket ${String(t["id"])} (${t["kind"]}, ${t["status"]})`);
|
|
61
|
+
console.log(` assignee: ${t["assignee"] ?? "-"}`);
|
|
62
|
+
console.log(` version: ${t["version_name"] ?? "?"} (${t["version_code"] ?? "?"}) · ${t["channel"] ?? "?"}`);
|
|
63
|
+
console.log(` device: ${t["device_model"] ?? "?"} · Android ${t["os_version"] ?? "?"} · ${t["arch"] ?? "?"} · ${t["locale"] ?? "?"}`);
|
|
64
|
+
console.log(` device_id: ${t["device_id"] ?? "-"}`);
|
|
65
|
+
console.log(` contact: ${t["contact"] ?? "-"}`);
|
|
66
|
+
console.log(` message:`);
|
|
67
|
+
console.log(String(t["message"] ?? "").split("\n").map((l) => " " + l).join("\n"));
|
|
68
|
+
if (res.attachments.length) {
|
|
69
|
+
console.log(` attachments:`);
|
|
70
|
+
for (const a of res.attachments) {
|
|
71
|
+
console.log(` ${a.id} ${a.filename} (${(a.size_bytes / 1024).toFixed(1)} KB)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (res.comments.length) {
|
|
75
|
+
console.log(` comments:`);
|
|
76
|
+
for (const cm of res.comments) {
|
|
77
|
+
console.log(` [${new Date(cm.created_at).toISOString()}] ${cm.author_actor}: ${cm.body}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
feedback
|
|
82
|
+
.command("update <appIdOrSlug> <ticketId>")
|
|
83
|
+
.description("Change status and/or assignee.")
|
|
84
|
+
.option("--status <status>", "open | in_progress | resolved | closed.")
|
|
85
|
+
.option("--assignee <name>", "Assign to a person/agent; use 'none' to unassign.")
|
|
86
|
+
.option("--json", "Output JSON.", false)
|
|
87
|
+
.action(async (appIdOrSlug, ticketId, opts) => {
|
|
88
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
89
|
+
const body = {};
|
|
90
|
+
if (opts.status)
|
|
91
|
+
body.status = opts.status;
|
|
92
|
+
if (opts.assignee !== undefined) {
|
|
93
|
+
body.assignee = opts.assignee === "none" ? null : opts.assignee;
|
|
94
|
+
}
|
|
95
|
+
if (body.status === undefined && body.assignee === undefined) {
|
|
96
|
+
throw new Error("nothing to update: pass --status and/or --assignee");
|
|
97
|
+
}
|
|
98
|
+
const res = await apiRequest(`/api/apps/${appId}/feedback/${ticketId}`, { method: "PATCH", body });
|
|
99
|
+
if (opts.json) {
|
|
100
|
+
console.log(JSON.stringify(res, null, 2));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(`Updated ticket ${ticketId.slice(0, 8)}.`);
|
|
104
|
+
});
|
|
105
|
+
feedback
|
|
106
|
+
.command("comment <appIdOrSlug> <ticketId> <text>")
|
|
107
|
+
.description("Add a comment to a ticket.")
|
|
108
|
+
.option("--json", "Output JSON.", false)
|
|
109
|
+
.action(async (appIdOrSlug, ticketId, text, opts) => {
|
|
110
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
111
|
+
const res = await apiRequest(`/api/apps/${appId}/feedback/${ticketId}/comments`, { method: "POST", body: { body: text } });
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify(res, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`Commented on ${ticketId.slice(0, 8)}.`);
|
|
117
|
+
});
|
|
118
|
+
feedback
|
|
119
|
+
.command("download-attachment <appIdOrSlug> <ticketId> <attachmentId>")
|
|
120
|
+
.alias("download")
|
|
121
|
+
.description("Download a feedback attachment to a file. Saves the raw bytes as-is — " +
|
|
122
|
+
"Quiver does not unzip or interpret the contents (the producing app owns the format).")
|
|
123
|
+
.option("-o, --output <path>", "Output file path. Defaults to the server filename in the cwd.")
|
|
124
|
+
.action(async (appIdOrSlug, ticketId, attachmentId, opts) => {
|
|
125
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
126
|
+
const res = await apiRequest(`/api/apps/${appId}/feedback/${ticketId}/attachments/${attachmentId}`, { raw: true });
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const detail = await res.text().catch(() => "");
|
|
129
|
+
console.error(`Download failed: ${res.status} ${res.statusText} ${detail.slice(0, 200)}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const disposition = res.headers.get("content-disposition") ?? "";
|
|
133
|
+
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
|
|
134
|
+
const serverName = match?.[1] ? decodeURIComponent(match[1]) : attachmentId;
|
|
135
|
+
const outPath = opts.output ?? serverName;
|
|
136
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
137
|
+
await writeFile(outPath, bytes);
|
|
138
|
+
console.log(`Saved ${bytes.length} bytes to ${outPath}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.js","sourceRoot":"","sources":["../../src/commands/feedback.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAgB3C,KAAK,UAAU,YAAY,CAAC,QAAgB;IAC1C,IAAI,QAAQ,CAAC,MAAM,KAAK,EAAE,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAChF,MAAM,GAAG,GAAG,MAAM,UAAU,CAAgD,WAAW,CAAC,CAAC;IACzF,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,qBAAqB,QAAQ,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC,EAAE,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAgB;IACvD,MAAM,QAAQ,GAAG,OAAO;SACrB,OAAO,CAAC,UAAU,CAAC;SACnB,WAAW,CAAC,gCAAgC,CAAC,CAAC;IAEjD,QAAQ;SACL,OAAO,CAAC,oBAAoB,CAAC;SAC7B,WAAW,CAAC,6BAA6B,CAAC;SAC1C,MAAM,CAAC,mBAAmB,EAAE,iDAAiD,CAAC;SAC9E,MAAM,CAAC,eAAe,EAAE,iCAAiC,CAAC;SAC1D,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC;SACvC,MAAM,CAAC,KAAK,EAAE,WAAmB,EAAE,IAAwD,EAAE,EAAE;QAC9F,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,MAAM;YAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,IAAI,IAAI,CAAC,IAAI;YAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,UAAU,CAC1B,aAAa,KAAK,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACnD,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5D,OAAO,CAAC,GAAG,CACT,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG;gBACpD,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,IAAI,GAAG,GAAG;gBAC9D,IAAI,CAAC,CAAC,gBAAgB,MAAM,CAAC,CAAC,aAAa,QAAQ,OAAO,EAAE,CAC/D,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,QAAQ;SACL,OAAO,CAAC,+BAA+B,CAAC;SACxC,WAAW,CAAC,+DAA+D,CAAC;SAC5E,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC;SACvC,MAAM,CAAC,KAAK,EAAE,WAAmB,EAAE,QAAgB,EAAE,IAAwB,EAAE,EAAE;QAChF,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,MAAM,UAAU,CAIzB,aAAa,KAAK,aAAa,QAAQ,EAAE,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,MAAM,CAAC,GAAG,GAAG,CAAC,MAAiC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACxE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,UAAU,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,cAAc,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAC9G,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,cAAc,CAAC,IAAI,GAAG,cAAc,CAAC,CAAC,YAAY,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QACzI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,WAAW,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACtF,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC9B,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;gBAChC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC3B,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;gBAC9B,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YAC/F,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,QAAQ;SACL,OAAO,CAAC,iCAAiC,CAAC;SAC1C,WAAW,CAAC,gCAAgC,CAAC;SAC7C,MAAM,CAAC,mBAAmB,EAAE,yCAAyC,CAAC;SACtE,MAAM,CAAC,mBAAmB,EAAE,mDAAmD,CAAC;SAChF,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC;SACvC,MAAM,CACL,KAAK,EACH,WAAmB,EACnB,QAAgB,EAChB,IAA4D,EAC5D,EAAE;QACF,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAkD,EAAE,CAAC;QAC/D,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3C,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;QAClE,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC7D,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,UAAU,CAC1B,aAAa,KAAK,aAAa,QAAQ,EAAE,EACzC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAC1B,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,kBAAkB,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC,CACF,CAAC;IAEJ,QAAQ;SACL,OAAO,CAAC,yCAAyC,CAAC;SAClD,WAAW,CAAC,4BAA4B,CAAC;SACzC,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC;SACvC,MAAM,CAAC,KAAK,EAAE,WAAmB,EAAE,QAAgB,EAAE,IAAY,EAAE,IAAwB,EAAE,EAAE;QAC9F,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,MAAM,UAAU,CAC1B,aAAa,KAAK,aAAa,QAAQ,WAAW,EAClD,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CACzC,CAAC;QACF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1C,OAAO;QACT,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEL,QAAQ;SACL,OAAO,CAAC,6DAA6D,CAAC;SACtE,KAAK,CAAC,UAAU,CAAC;SACjB,WAAW,CACV,wEAAwE;QACtE,sFAAsF,CACzF;SACA,MAAM,CAAC,qBAAqB,EAAE,+DAA+D,CAAC;SAC9F,MAAM,CACL,KAAK,EACH,WAAmB,EACnB,QAAgB,EAChB,YAAoB,EACpB,IAAyB,EACzB,EAAE;QACF,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,GAAG,GAAG,MAAM,UAAU,CAC1B,aAAa,KAAK,aAAa,QAAQ,gBAAgB,YAAY,EAAE,EACrE,EAAE,GAAG,EAAE,IAAI,EAAE,CACd,CAAC;QACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAChD,OAAO,CAAC,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAC;QACjE,MAAM,KAAK,GAAG,uCAAuC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxE,MAAM,UAAU,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;QAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QACnD,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,MAAM,aAAa,OAAO,EAAE,CAAC,CAAC;IAC3D,CAAC,CACF,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver login` — authenticate the CLI.
|
|
3
|
+
*
|
|
4
|
+
* v1 flow (browser-required):
|
|
5
|
+
* 1. CLI prints a URL: https://quiver-worker.../api/auth/login?return_to=...
|
|
6
|
+
* 2. User opens the URL in any browser, signs in with Raft OAuth.
|
|
7
|
+
* 3. After login the Worker redirects to /login/raft/callback which is the
|
|
8
|
+
* admin SPA — at this point the user has a HttpOnly `quiver_session`
|
|
9
|
+
* cookie in their browser for the Worker's origin.
|
|
10
|
+
* 4. User opens DevTools → Application → Cookies → copies the cookie value.
|
|
11
|
+
* 5. User runs `quiver login --token <cookie>` (or pastes when prompted).
|
|
12
|
+
*
|
|
13
|
+
* CI mode: `QUIVER_SESSION_COOKIE=... quiver whoami` — env var is read
|
|
14
|
+
* directly, no file storage.
|
|
15
|
+
*
|
|
16
|
+
* Why not OAuth Device Flow or PKCE? Raft OAuth today only supports the
|
|
17
|
+
* browser redirect flow with HttpOnly cookies; the CLI can't intercept
|
|
18
|
+
* the callback. Headless flow is a v2 (TBD: Raft Device Flow support or
|
|
19
|
+
* dev-token bypass for service users).
|
|
20
|
+
*/
|
|
21
|
+
import type { Command } from "commander";
|
|
22
|
+
export declare function registerLoginCommands(program: Command): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver login` — authenticate the CLI.
|
|
3
|
+
*
|
|
4
|
+
* v1 flow (browser-required):
|
|
5
|
+
* 1. CLI prints a URL: https://quiver-worker.../api/auth/login?return_to=...
|
|
6
|
+
* 2. User opens the URL in any browser, signs in with Raft OAuth.
|
|
7
|
+
* 3. After login the Worker redirects to /login/raft/callback which is the
|
|
8
|
+
* admin SPA — at this point the user has a HttpOnly `quiver_session`
|
|
9
|
+
* cookie in their browser for the Worker's origin.
|
|
10
|
+
* 4. User opens DevTools → Application → Cookies → copies the cookie value.
|
|
11
|
+
* 5. User runs `quiver login --token <cookie>` (or pastes when prompted).
|
|
12
|
+
*
|
|
13
|
+
* CI mode: `QUIVER_SESSION_COOKIE=... quiver whoami` — env var is read
|
|
14
|
+
* directly, no file storage.
|
|
15
|
+
*
|
|
16
|
+
* Why not OAuth Device Flow or PKCE? Raft OAuth today only supports the
|
|
17
|
+
* browser redirect flow with HttpOnly cookies; the CLI can't intercept
|
|
18
|
+
* the callback. Headless flow is a v2 (TBD: Raft Device Flow support or
|
|
19
|
+
* dev-token bypass for service users).
|
|
20
|
+
*/
|
|
21
|
+
import { createInterface } from "node:readline/promises";
|
|
22
|
+
import { stdin, stdout } from "node:process";
|
|
23
|
+
import { apiRequest, getApiBase, QuiverApiError } from "../lib/api.js";
|
|
24
|
+
import { saveConfig, getConfig } from "../lib/config.js";
|
|
25
|
+
async function promptSecret(message) {
|
|
26
|
+
// Use a raw-mode readline so we can mask input with '*'.
|
|
27
|
+
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
|
|
28
|
+
process.stdout.write(message);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let input = "";
|
|
31
|
+
const onData = (chunk) => {
|
|
32
|
+
const ch = chunk.toString();
|
|
33
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
34
|
+
process.stdin.removeListener("data", onData);
|
|
35
|
+
process.stdout.write("\n");
|
|
36
|
+
rl.close();
|
|
37
|
+
resolve(input);
|
|
38
|
+
}
|
|
39
|
+
else if (ch === "\u0003") {
|
|
40
|
+
// Ctrl+C
|
|
41
|
+
process.stdout.write("\n");
|
|
42
|
+
rl.close();
|
|
43
|
+
reject(new Error("cancelled"));
|
|
44
|
+
}
|
|
45
|
+
else if (ch === "\u007f" || ch === "\b") {
|
|
46
|
+
// Backspace
|
|
47
|
+
if (input.length > 0) {
|
|
48
|
+
input = input.slice(0, -1);
|
|
49
|
+
process.stdout.write("\b \b");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
input += ch;
|
|
54
|
+
process.stdout.write("*");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
process.stdin.on("data", onData);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function registerLoginCommands(program) {
|
|
61
|
+
const cmd = program
|
|
62
|
+
.command("login")
|
|
63
|
+
.description("Authenticate the CLI against the Quiver Worker.")
|
|
64
|
+
.option("--token <cookie>", "Paste the quiver_session cookie value (from your browser's DevTools).")
|
|
65
|
+
.option("--api <url>", "Override the Quiver Worker base URL for this login only.")
|
|
66
|
+
.option("--print-url", "Just print the login URL; don't prompt for a token.", false)
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
const apiBase = opts.api ?? getApiBase();
|
|
69
|
+
const loginUrl = `${apiBase}/api/auth/login?return_to=${encodeURIComponent("/cli/callback")}`;
|
|
70
|
+
if (opts.printUrl) {
|
|
71
|
+
console.log(loginUrl);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log("To authenticate the quiver CLI:");
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(` 1. Open this URL in any browser:`);
|
|
77
|
+
console.log(` ${loginUrl}`);
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(` 2. Sign in with Raft. You'll land on the admin UI.`);
|
|
80
|
+
console.log(` 3. Open DevTools → Application → Cookies → copy the value of "quiver_session".`);
|
|
81
|
+
console.log(` 4. Paste it below.`);
|
|
82
|
+
console.log("");
|
|
83
|
+
let token = opts.token;
|
|
84
|
+
if (!token) {
|
|
85
|
+
token = await promptSecret("quiver_session cookie value (input is hidden): ");
|
|
86
|
+
if (token.length < 8) {
|
|
87
|
+
console.error("Token looks too short (min 8 chars).");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Persist the token + apiBase to config file.
|
|
92
|
+
saveConfig({ apiBase, sessionCookie: token });
|
|
93
|
+
console.log(`✔ Saved to ${configDisplayPath()}`);
|
|
94
|
+
console.log(` API base: ${apiBase}`);
|
|
95
|
+
// Verify the token works by calling /api/auth/me.
|
|
96
|
+
try {
|
|
97
|
+
await apiRequest("/api/auth/me");
|
|
98
|
+
console.log(`✔ Token verified — you're logged in.`);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (e instanceof QuiverApiError && e.status === 401) {
|
|
102
|
+
console.error(`✘ Token rejected (401). Run \`quiver logout\` and try again.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
program
|
|
109
|
+
.command("logout")
|
|
110
|
+
.description("Clear the saved session cookie.")
|
|
111
|
+
.action(() => {
|
|
112
|
+
const cfg = getConfig();
|
|
113
|
+
if (!cfg.sessionCookie) {
|
|
114
|
+
console.log("Not logged in (no saved session cookie).");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
delete cfg.sessionCookie;
|
|
118
|
+
saveConfig(cfg);
|
|
119
|
+
console.log(`✔ Logged out (token cleared from ${configDisplayPath()}).`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function configDisplayPath() {
|
|
123
|
+
// Mirror getConfig's path resolution for display only.
|
|
124
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
125
|
+
const dir = xdg && xdg.length > 0 ? xdg : `${process.env.HOME ?? "~"}/.config`;
|
|
126
|
+
return `${dir}/quiver/auth.json`;
|
|
127
|
+
}
|
|
128
|
+
// silence "unused import" if user has no cookie in env
|
|
129
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quiver releases` — release operations that are not part of build publish.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { apiRequest } from "../lib/api.js";
|
|
6
|
+
import { readEnv } from "../lib/env.js";
|
|
7
|
+
const DEFAULT_SHARE_TTL_SECONDS = "604800";
|
|
8
|
+
export function registerReleaseCommands(program) {
|
|
9
|
+
const releases = program
|
|
10
|
+
.command("releases")
|
|
11
|
+
.description("Manage release shares.");
|
|
12
|
+
releases
|
|
13
|
+
.command("show <appIdOrSlug> <releaseId>")
|
|
14
|
+
.description("Show a release (status, changelog, rollout) for review.")
|
|
15
|
+
.option("--json", "Output JSON.", false)
|
|
16
|
+
.action(async (appIdOrSlug, releaseId, opts) => {
|
|
17
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
18
|
+
const detail = await apiRequest(`/api/apps/${appId}/releases/${releaseId}`);
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const r = detail.release;
|
|
24
|
+
console.log(`Release ${r.id}`);
|
|
25
|
+
console.log(` status: ${r.status}`);
|
|
26
|
+
console.log(` rollout: ${r.rollout_cohort_count ?? 100}%`);
|
|
27
|
+
console.log(` changelog:`);
|
|
28
|
+
console.log((r.changelog ?? "(none)").split("\n").map((l) => " " + l).join("\n"));
|
|
29
|
+
});
|
|
30
|
+
releases
|
|
31
|
+
.command("update <appIdOrSlug> <releaseId>")
|
|
32
|
+
.description("Update a draft/active release; use to write the reviewed changelog before publish.")
|
|
33
|
+
.option("--changelog <text>", "Changelog text. Repeatable with lang=text for multiple languages.", (value, prev = []) => [...prev, value])
|
|
34
|
+
.option("--changelog-file <path>", "Changelog file. Repeatable with lang=path, e.g. --changelog-file zh=zh.md --changelog-file en=en.md.", (value, prev = []) => [...prev, value])
|
|
35
|
+
.option("--json", "Output JSON.", false)
|
|
36
|
+
.action(async (appIdOrSlug, releaseId, opts) => {
|
|
37
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
38
|
+
// Each entry is either "text/path" (single-language plain changelog)
|
|
39
|
+
// or "lang=text/path". Language keys are normalized: zh -> zh-CN.
|
|
40
|
+
const langAliases = { zh: "zh-CN", cn: "zh-CN" };
|
|
41
|
+
const byLang = {};
|
|
42
|
+
let plain;
|
|
43
|
+
const consume = (entry, fromFile) => {
|
|
44
|
+
const eq = entry.indexOf("=");
|
|
45
|
+
if (eq > 0 && eq <= 10) {
|
|
46
|
+
const langRaw = entry.slice(0, eq).trim().toLowerCase();
|
|
47
|
+
const lang = langAliases[langRaw] ?? langRaw;
|
|
48
|
+
const value = entry.slice(eq + 1);
|
|
49
|
+
byLang[lang] = (fromFile ? readFileSync(value, "utf8") : value).trim();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
plain = (fromFile ? readFileSync(entry, "utf8") : entry).trim();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (const entry of opts.changelog ?? [])
|
|
56
|
+
consume(entry, false);
|
|
57
|
+
for (const entry of opts.changelogFile ?? [])
|
|
58
|
+
consume(entry, true);
|
|
59
|
+
let changelog;
|
|
60
|
+
const langs = Object.keys(byLang);
|
|
61
|
+
if (langs.length > 0) {
|
|
62
|
+
if (plain !== undefined) {
|
|
63
|
+
throw new Error("mix of plain and lang= changelog entries; pick one style");
|
|
64
|
+
}
|
|
65
|
+
changelog = JSON.stringify(byLang);
|
|
66
|
+
}
|
|
67
|
+
else if (plain !== undefined) {
|
|
68
|
+
changelog = plain;
|
|
69
|
+
}
|
|
70
|
+
if (changelog === undefined) {
|
|
71
|
+
throw new Error("nothing to update: pass --changelog(-file) [lang=]value, e.g. --changelog-file zh=zh.md --changelog-file en=en.md");
|
|
72
|
+
}
|
|
73
|
+
const updated = await apiRequest(`/api/apps/${appId}/releases/${releaseId}`, { method: "PATCH", body: { changelog } });
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`Updated release ${releaseId} changelog${langs.length ? ` (${langs.join(", ")})` : ""}.`);
|
|
79
|
+
});
|
|
80
|
+
releases
|
|
81
|
+
.command("publish <appIdOrSlug> <releaseId>")
|
|
82
|
+
.description("Publish a draft release (the explicit human/agent step after changelog review).")
|
|
83
|
+
.option("--json", "Output JSON.", false)
|
|
84
|
+
.action(async (appIdOrSlug, releaseId, opts) => {
|
|
85
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
86
|
+
const result = await apiRequest(`/api/apps/${appId}/releases/${releaseId}/publish`, { method: "POST", body: {} });
|
|
87
|
+
if (opts.json) {
|
|
88
|
+
console.log(JSON.stringify(result, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(`Published release ${releaseId}.`);
|
|
92
|
+
});
|
|
93
|
+
releases
|
|
94
|
+
.command("share <appIdOrSlug> <releaseId>")
|
|
95
|
+
.description("Create a revocable public share page for a release.")
|
|
96
|
+
.option("--ttl-seconds <seconds>", "Share lifetime in seconds.", DEFAULT_SHARE_TTL_SECONDS)
|
|
97
|
+
.option("--expires-at <millis>", "Absolute expiration as Unix milliseconds.")
|
|
98
|
+
.option("--password <password>", "Password-protect the share page (or set QUIVER_SHARE_PASSWORD to keep it out of shell history).")
|
|
99
|
+
.option("--json", "Output JSON.", false)
|
|
100
|
+
.action(async (appIdOrSlug, releaseId, opts) => {
|
|
101
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
102
|
+
const body = {};
|
|
103
|
+
if (opts.expiresAt) {
|
|
104
|
+
body.expires_at = parsePositiveNumber(opts.expiresAt, "--expires-at");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
body.ttl_seconds = parsePositiveNumber(opts.ttlSeconds ?? DEFAULT_SHARE_TTL_SECONDS, "--ttl-seconds");
|
|
108
|
+
}
|
|
109
|
+
const password = opts.password ?? readEnv("SHARE_PASSWORD");
|
|
110
|
+
if (password)
|
|
111
|
+
body.password = password;
|
|
112
|
+
const share = await apiRequest(`/api/apps/${appId}/releases/${releaseId}/shares`, { method: "POST", body });
|
|
113
|
+
if (opts.json) {
|
|
114
|
+
console.log(JSON.stringify(share, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`Created release share ${share.id}`);
|
|
118
|
+
console.log(` url: ${share.share_url ?? ""}`);
|
|
119
|
+
console.log(` expires_at: ${new Date(share.expires_at).toISOString()}`);
|
|
120
|
+
if (body.password)
|
|
121
|
+
console.log(" password: protected");
|
|
122
|
+
});
|
|
123
|
+
releases
|
|
124
|
+
.command("shares <appIdOrSlug> <releaseId>")
|
|
125
|
+
.description("List public shares for a release.")
|
|
126
|
+
.option("--json", "Output JSON.", false)
|
|
127
|
+
.action(async (appIdOrSlug, releaseId, opts) => {
|
|
128
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
129
|
+
const res = await apiRequest(`/api/apps/${appId}/releases/${releaseId}/shares`);
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
console.log(JSON.stringify(res, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (res.shares.length === 0) {
|
|
135
|
+
console.log("No release shares.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const share of res.shares) {
|
|
139
|
+
const state = share.revoked_at ? "revoked" : Date.now() >= share.expires_at ? "expired" : "active";
|
|
140
|
+
console.log(`${share.id} ${state} expires=${new Date(share.expires_at).toISOString()}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
releases
|
|
144
|
+
.command("update-share <appIdOrSlug> <releaseId> <shareId>")
|
|
145
|
+
.description("Renew or change a public release share expiration.")
|
|
146
|
+
.option("--ttl-seconds <seconds>", "New lifetime in seconds from now.", DEFAULT_SHARE_TTL_SECONDS)
|
|
147
|
+
.option("--expires-at <millis>", "Absolute expiration as Unix milliseconds.")
|
|
148
|
+
.option("--json", "Output JSON.", false)
|
|
149
|
+
.action(async (appIdOrSlug, releaseId, shareId, opts) => {
|
|
150
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
151
|
+
const body = {};
|
|
152
|
+
if (opts.expiresAt) {
|
|
153
|
+
body.expires_at = parsePositiveNumber(opts.expiresAt, "--expires-at");
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
body.ttl_seconds = parsePositiveNumber(opts.ttlSeconds ?? DEFAULT_SHARE_TTL_SECONDS, "--ttl-seconds");
|
|
157
|
+
}
|
|
158
|
+
const share = await apiRequest(`/api/apps/${appId}/releases/${releaseId}/shares/${shareId}`, { method: "PATCH", body });
|
|
159
|
+
if (opts.json) {
|
|
160
|
+
console.log(JSON.stringify(share, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
console.log(`Updated release share ${share.id}`);
|
|
164
|
+
console.log(` expires_at: ${new Date(share.expires_at).toISOString()}`);
|
|
165
|
+
});
|
|
166
|
+
releases
|
|
167
|
+
.command("revoke-share <appIdOrSlug> <releaseId> <shareId>")
|
|
168
|
+
.description("Revoke a public release share.")
|
|
169
|
+
.option("--json", "Output JSON.", false)
|
|
170
|
+
.action(async (appIdOrSlug, releaseId, shareId, opts) => {
|
|
171
|
+
const appId = await resolveAppId(appIdOrSlug);
|
|
172
|
+
const res = await apiRequest(`/api/apps/${appId}/releases/${releaseId}/shares/${shareId}`, { method: "DELETE" });
|
|
173
|
+
if (opts.json) {
|
|
174
|
+
console.log(JSON.stringify(res, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(`Revoked release share ${res.id}`);
|
|
178
|
+
console.log(` revoked_at: ${new Date(res.revoked_at).toISOString()}`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function resolveAppId(slugOrId) {
|
|
182
|
+
if (slugOrId.length === 36 && slugOrId.split("-").length === 5) {
|
|
183
|
+
return slugOrId;
|
|
184
|
+
}
|
|
185
|
+
const res = await apiRequest("/api/apps");
|
|
186
|
+
const match = res.apps.find((a) => a.slug === slugOrId);
|
|
187
|
+
if (!match) {
|
|
188
|
+
console.error(`No app with slug '${slugOrId}'.`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
return match.id;
|
|
192
|
+
}
|
|
193
|
+
function parsePositiveNumber(value, flag) {
|
|
194
|
+
const parsed = Number(value);
|
|
195
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
196
|
+
throw new Error(`${flag} must be a positive number`);
|
|
197
|
+
}
|
|
198
|
+
return Math.floor(parsed);
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=releases.js.map
|