@akanjs/cli 3.1.0-rc.0 → 3.1.0-rc.10
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/.build-stamp +1 -1
- package/{CodeAgent-q7a4srt6.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-ak23dxc9.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-3v2d53gd.js → application.command-m1kcd7jf.js} +54 -42
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
- package/buildBatch.proc.js +24 -15
- package/{cloud.command-kk2rwkvw.js → cloud.command-9cp1t0f5.js} +63 -65
- package/cloud.script-dgstqt1m.js +24 -0
- package/{code.command-30sca09d.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-k7eqacq6.js → context.command-0qx447mb.js} +17 -13
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +3 -3
- package/guidelines/conventions/conventions.instruction.md +457 -1069
- package/guidelines/cssRule/cssRule.instruction.md +28 -7
- package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +48 -20
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +44 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +135 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
- package/incrementalBuilder.proc.js +10 -7
- package/index-192ha4xh.js +1173 -0
- package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
- package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
- package/{index-nz15k0b6.js → index-20bxxyvn.js} +80 -80
- package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
- package/{index-jyv387qw.js → index-4mhy6qme.js} +4 -4
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/index-665zp9rs.js +271 -0
- package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
- package/{index-2t4tqd1c.js → index-8fp3b47h.js} +419 -418
- package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
- package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-tn4rpqp1.js → index-bspm21y1.js} +73 -49
- package/index-cpb3nvma.js +667 -0
- package/{index-p9ystjrp.js → index-d47ypebv.js} +97 -55
- package/{index-0bm04n5g.js → index-d72hfmdm.js} +15 -13
- package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
- package/{index-y8q31011.js → index-fyq1z6kt.js} +68 -177
- package/{index-kjvrq3nc.js → index-g1mbppa9.js} +331 -250
- package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
- package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
- package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/index-mdjc93e1.js +33 -0
- package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
- package/{index-7bymavqk.js → index-qwa5k1j8.js} +1 -1
- package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
- package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
- package/index-vk0pa0mn.js +240 -0
- package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-9e84e32q.js → localRegistry.command-y176p558.js} +15 -11
- package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
- package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-f02j5tge.js → subspace.command-5c7yasd7.js} +424 -364
- package/templates/app/env/env.client.type.ts +2 -3
- package/templates/app/page/styles.css.template +8 -0
- package/templates/appSample/lib/task/Task.Util.tsx +7 -7
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
- package/templates/appSample/ui/QuantityControl.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +23 -52
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{tunnel.command-9x8nfn75.js → tunnel.command-2kw8vt9g.js} +8 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-cmj6mqyk.js → workspace.command-6qfgq4b4.js} +38 -34
- package/TunnelShare-21mm6dr5.js +0 -9
- package/cloud.script-r7eyfey4.js +0 -20
- package/context.runner-hqd1ppqe.js +0 -21
- package/index-6w0rbr9d.js +0 -68
- package/index-n68jx79w.js +0 -120
- package/index-r2tpeb3n.js +0 -392
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
FileSys2
|
|
4
|
+
} from "./index-52w0fvh8.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveMobileTargets
|
|
7
|
+
} from "./index-m9jj544v.js";
|
|
8
|
+
import {
|
|
9
|
+
NativePublicEnv,
|
|
10
|
+
UpdatePackFolder,
|
|
11
|
+
NativeApp
|
|
12
|
+
} from "./index-g1mbppa9.js";
|
|
13
|
+
import {
|
|
14
|
+
ExitCodeError
|
|
15
|
+
} from "./index-jw1zm273.js";
|
|
16
|
+
import {
|
|
17
|
+
ConfigLock
|
|
18
|
+
} from "./index-bqbf654f.js";
|
|
19
|
+
|
|
20
|
+
// pkgs/@akanjs/devkit/cloud/cloudHttpError.ts
|
|
21
|
+
import { isRecord } from "akanjs/common";
|
|
22
|
+
|
|
23
|
+
class CloudHttpError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
statusText;
|
|
26
|
+
url;
|
|
27
|
+
body;
|
|
28
|
+
headers;
|
|
29
|
+
static #errKey = /^[\w$-]+\.error\.[\w$-]+$/;
|
|
30
|
+
name = "CloudHttpError";
|
|
31
|
+
constructor(status, statusText, url, body, headers = new Headers) {
|
|
32
|
+
super(`${status} ${statusText} from ${url}: ${body.slice(0, 200)}`);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.statusText = statusText;
|
|
35
|
+
this.url = url;
|
|
36
|
+
this.body = body;
|
|
37
|
+
this.headers = headers;
|
|
38
|
+
}
|
|
39
|
+
static async of(response, url) {
|
|
40
|
+
return new CloudHttpError(response.status, response.statusText, url, await response.text(), response.headers);
|
|
41
|
+
}
|
|
42
|
+
static statusOf(error) {
|
|
43
|
+
return error instanceof CloudHttpError ? error.status : null;
|
|
44
|
+
}
|
|
45
|
+
get error() {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(this.body);
|
|
48
|
+
return isRecord(parsed) && typeof parsed.error === "string" ? parsed.error : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
get errKey() {
|
|
54
|
+
const { error } = this;
|
|
55
|
+
return error && CloudHttpError.#errKey.test(error) ? error : null;
|
|
56
|
+
}
|
|
57
|
+
get retryAfterMs() {
|
|
58
|
+
return CloudHttpError.retryAfterMs(this.headers.get("retry-after"));
|
|
59
|
+
}
|
|
60
|
+
static retryAfterMs(header) {
|
|
61
|
+
const value = header?.trim() ?? "";
|
|
62
|
+
if (!value)
|
|
63
|
+
return null;
|
|
64
|
+
const ms = /^\d+$/.test(value) ? Number(value) * 1000 : Date.parse(value) - Date.now();
|
|
65
|
+
return Number.isFinite(ms) ? Math.max(0, ms) : null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// pkgs/@akanjs/devkit/cloud/globalConfig.ts
|
|
70
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
71
|
+
import { trimTrailingSlashes } from "akanjs/common";
|
|
72
|
+
import dayjs from "dayjs";
|
|
73
|
+
|
|
74
|
+
// pkgs/@akanjs/devkit/cloud/constants.ts
|
|
75
|
+
import path from "path";
|
|
76
|
+
var basePath = path.join(Bun.env.HOME ?? Bun.env.USERPROFILE ?? "", ".akan");
|
|
77
|
+
var configPath = path.join(basePath, "config.json");
|
|
78
|
+
var getDefaultHostConfig = (host = GlobalConfig.akanCloudHost) => ({ host });
|
|
79
|
+
var defaultAkanGlobalConfig = {
|
|
80
|
+
cloudHost: {},
|
|
81
|
+
remoteEnvServers: {},
|
|
82
|
+
testTargets: {}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// pkgs/@akanjs/devkit/cloud/globalConfig.ts
|
|
86
|
+
class GlobalConfig {
|
|
87
|
+
static akanCloudHost = process.env.USE_AKANJS_PKGS === "true" ? `http://localhost:${process.env.CLOUD_HOST_PORT ?? 8283}` : "https://cloud.akanjs.com";
|
|
88
|
+
static async#getAkanGlobalConfig() {
|
|
89
|
+
const exists = await FileSys2.fileExists(configPath);
|
|
90
|
+
const akanConfig = exists ? await FileSys2.readJson(configPath) : {};
|
|
91
|
+
return {
|
|
92
|
+
...defaultAkanGlobalConfig,
|
|
93
|
+
...akanConfig,
|
|
94
|
+
cloudHost: akanConfig.cloudHost ?? defaultAkanGlobalConfig.cloudHost,
|
|
95
|
+
remoteEnvServers: akanConfig.remoteEnvServers ?? defaultAkanGlobalConfig.remoteEnvServers,
|
|
96
|
+
testTargets: akanConfig.testTargets ?? defaultAkanGlobalConfig.testTargets
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
static async#setAkanGlobalConfig(akanConfig) {
|
|
100
|
+
await mkdir(basePath, { recursive: true, mode: 448 });
|
|
101
|
+
const temp = `${configPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
102
|
+
try {
|
|
103
|
+
await writeFile(temp, JSON.stringify(akanConfig, null, 2), { mode: 384 });
|
|
104
|
+
await FileSys2.replace(temp, configPath);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
await rm(temp, { force: true });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
static #lockPath = `${configPath}.lock`;
|
|
111
|
+
static async#update(mutate) {
|
|
112
|
+
await ConfigLock.run(GlobalConfig.#lockPath, async () => {
|
|
113
|
+
await GlobalConfig.#setAkanGlobalConfig(mutate(await GlobalConfig.#getAkanGlobalConfig()));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
static async hostFor(workspace, host) {
|
|
117
|
+
return GlobalConfig.#normalizeHost(host || ((await workspace.getConfig()).cloudHost ?? GlobalConfig.akanCloudHost));
|
|
118
|
+
}
|
|
119
|
+
static #normalizeHost(host) {
|
|
120
|
+
const url = URL.canParse(host) ? new URL(host) : null;
|
|
121
|
+
return url && (url.protocol === "https:" || url.protocol === "http:") ? `${url.origin}${trimTrailingSlashes(url.pathname)}` : trimTrailingSlashes(host);
|
|
122
|
+
}
|
|
123
|
+
static async getHostConfig(host = GlobalConfig.akanCloudHost) {
|
|
124
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
125
|
+
return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
|
|
126
|
+
}
|
|
127
|
+
static async setHostConfig(config = getDefaultHostConfig()) {
|
|
128
|
+
await GlobalConfig.#update((akanConfig) => GlobalConfig.#withHost(akanConfig, config));
|
|
129
|
+
}
|
|
130
|
+
static #withHost(akanConfig, config) {
|
|
131
|
+
return {
|
|
132
|
+
...akanConfig,
|
|
133
|
+
cloudHost: { ...akanConfig.cloudHost, [config.host]: GlobalConfig.toHostConfigDto(config) }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
static async refreshHostAuth(host, refresh) {
|
|
137
|
+
try {
|
|
138
|
+
return await ConfigLock.run(GlobalConfig.#lockPath, async () => {
|
|
139
|
+
const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
|
|
140
|
+
const current = GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? getDefaultHostConfig(host));
|
|
141
|
+
const accessToken = current.auth?.accessToken;
|
|
142
|
+
const refreshToken = accessToken?.refreshToken;
|
|
143
|
+
if (!accessToken || !refreshToken || !GlobalConfig.needRefreshToken(accessToken))
|
|
144
|
+
return current;
|
|
145
|
+
const claimed = {
|
|
146
|
+
...current,
|
|
147
|
+
auth: { ...current.auth, accessToken: { ...accessToken, refreshToken: null } }
|
|
148
|
+
};
|
|
149
|
+
await GlobalConfig.#setAkanGlobalConfig(GlobalConfig.#withHost(akanConfig, claimed));
|
|
150
|
+
const next = await refresh(refreshToken).catch(() => null);
|
|
151
|
+
if (!next?.jwt)
|
|
152
|
+
return claimed;
|
|
153
|
+
const refreshed = { ...current, auth: { ...current.auth, accessToken: next } };
|
|
154
|
+
await GlobalConfig.#setAkanGlobalConfig(GlobalConfig.#withHost(await GlobalConfig.#getAkanGlobalConfig(), refreshed));
|
|
155
|
+
return refreshed;
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
return await GlobalConfig.getHostConfig(host);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static async getRemoteEnvServers() {
|
|
162
|
+
return (await GlobalConfig.#getAkanGlobalConfig()).remoteEnvServers;
|
|
163
|
+
}
|
|
164
|
+
static async setRemoteEnvServer(name, config) {
|
|
165
|
+
await GlobalConfig.#update((akanConfig) => ({
|
|
166
|
+
...akanConfig,
|
|
167
|
+
remoteEnvServers: { ...akanConfig.remoteEnvServers, [name]: config }
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
static async removeRemoteEnvServer(name) {
|
|
171
|
+
await GlobalConfig.#update((akanConfig) => {
|
|
172
|
+
const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
|
|
173
|
+
return { ...akanConfig, remoteEnvServers };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
static async getTestTargets() {
|
|
177
|
+
return (await GlobalConfig.#getAkanGlobalConfig()).testTargets;
|
|
178
|
+
}
|
|
179
|
+
static async setTestTargets(testTargets) {
|
|
180
|
+
await GlobalConfig.#update((akanConfig) => ({
|
|
181
|
+
...akanConfig,
|
|
182
|
+
testTargets: { ...akanConfig.testTargets, ...testTargets }
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
static needRefreshToken(accessToken) {
|
|
186
|
+
return !!accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
|
|
187
|
+
}
|
|
188
|
+
static toAccessToken(accessToken) {
|
|
189
|
+
return {
|
|
190
|
+
jwt: accessToken.jwt,
|
|
191
|
+
refreshToken: accessToken.refreshToken ?? null,
|
|
192
|
+
expiresAt: accessToken.expiresAt ? dayjs(accessToken.expiresAt) : null
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
static toAccessTokenDto(accessToken) {
|
|
196
|
+
return {
|
|
197
|
+
jwt: accessToken.jwt,
|
|
198
|
+
refreshToken: accessToken.refreshToken ?? null,
|
|
199
|
+
expiresAt: accessToken.expiresAt?.toString() ?? null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
static toHostConfigDto(hostConfig) {
|
|
203
|
+
return {
|
|
204
|
+
host: hostConfig.host,
|
|
205
|
+
auth: {
|
|
206
|
+
accessToken: hostConfig.auth?.accessToken ? GlobalConfig.toAccessTokenDto(hostConfig.auth.accessToken) : undefined,
|
|
207
|
+
self: hostConfig.auth?.self
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
static toHostConfig(hostConfigDto) {
|
|
212
|
+
return {
|
|
213
|
+
host: hostConfigDto.host,
|
|
214
|
+
auth: {
|
|
215
|
+
accessToken: hostConfigDto.auth?.accessToken ? GlobalConfig.toAccessToken(hostConfigDto.auth.accessToken) : undefined,
|
|
216
|
+
self: hostConfigDto.auth?.self
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// pkgs/@akanjs/devkit/cloud/cloudApi.ts
|
|
223
|
+
class HttpClient {
|
|
224
|
+
baseUrl;
|
|
225
|
+
headers = {};
|
|
226
|
+
constructor(baseUrl, headers = {}) {
|
|
227
|
+
this.baseUrl = baseUrl;
|
|
228
|
+
this.headers = headers;
|
|
229
|
+
}
|
|
230
|
+
async get(url, { headers, signal } = {}) {
|
|
231
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
232
|
+
headers: { "Content-Type": "application/json", ...this.headers, ...headers },
|
|
233
|
+
...signal ? { signal } : {}
|
|
234
|
+
});
|
|
235
|
+
return await HttpClient.#body(response, url);
|
|
236
|
+
}
|
|
237
|
+
async getFile(url, localPath, headers) {
|
|
238
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
239
|
+
headers: { ...this.headers, ...headers }
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok)
|
|
242
|
+
throw await CloudHttpError.of(response, url);
|
|
243
|
+
await Bun.write(localPath, response);
|
|
244
|
+
}
|
|
245
|
+
async post(url, data, { headers, signal } = {}) {
|
|
246
|
+
const isFormData = data instanceof FormData;
|
|
247
|
+
const response = await fetch(`${this.baseUrl}${url}`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: isFormData ? data : JSON.stringify(data),
|
|
250
|
+
headers: isFormData ? { ...this.headers, ...headers } : { "Content-Type": "application/json", ...this.headers, ...headers },
|
|
251
|
+
...signal ? { signal } : {}
|
|
252
|
+
});
|
|
253
|
+
return await HttpClient.#body(response, url);
|
|
254
|
+
}
|
|
255
|
+
static async#body(response, url) {
|
|
256
|
+
if (!response.ok)
|
|
257
|
+
throw await CloudHttpError.of(response, url);
|
|
258
|
+
return JSON.parse(await response.text());
|
|
259
|
+
}
|
|
260
|
+
setHeaders(headers) {
|
|
261
|
+
Object.assign(this.headers, headers);
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class CloudApi {
|
|
267
|
+
static refusal(error, who, { unavailable } = {}) {
|
|
268
|
+
if (!(error instanceof CloudHttpError))
|
|
269
|
+
return error;
|
|
270
|
+
if (error.status === 401)
|
|
271
|
+
return new Error("The server did not accept your sign-in: run `akan login`, then try again.", { cause: error });
|
|
272
|
+
if (error.status === 403)
|
|
273
|
+
return new Error(`The project refused: ${who}.`, { cause: error });
|
|
274
|
+
if (error.status === 400)
|
|
275
|
+
return new Error(`The project refused the request${error.error ? `: ${error.error}` : ""}.`, { cause: error });
|
|
276
|
+
const { errKey } = error;
|
|
277
|
+
if (error.status === 503 && unavailable && errKey && errKey !== "base.error.serverUnavailable")
|
|
278
|
+
return new Error(`${unavailable}: ${errKey}.`, { cause: error });
|
|
279
|
+
return error;
|
|
280
|
+
}
|
|
281
|
+
#api;
|
|
282
|
+
#accessToken = null;
|
|
283
|
+
#workspace;
|
|
284
|
+
host;
|
|
285
|
+
url;
|
|
286
|
+
static async fromHost(workspace, host) {
|
|
287
|
+
const hostConfig = await GlobalConfig.getHostConfig(host);
|
|
288
|
+
const accessToken = hostConfig.auth?.accessToken;
|
|
289
|
+
if (!accessToken || !GlobalConfig.needRefreshToken(accessToken))
|
|
290
|
+
return new CloudApi(workspace, hostConfig);
|
|
291
|
+
const tokenless = new CloudApi(workspace, hostConfig);
|
|
292
|
+
const refreshed = await GlobalConfig.refreshHostAuth(hostConfig.host, (refreshToken) => tokenless.refreshAuthToken(refreshToken));
|
|
293
|
+
return new CloudApi(workspace, refreshed);
|
|
294
|
+
}
|
|
295
|
+
constructor(workspace, hostConfig) {
|
|
296
|
+
this.#workspace = workspace;
|
|
297
|
+
this.#accessToken = hostConfig.auth?.accessToken ?? null;
|
|
298
|
+
this.host = hostConfig.host;
|
|
299
|
+
this.url = `${this.host}/api`;
|
|
300
|
+
this.#api = new HttpClient(this.url);
|
|
301
|
+
if (this.#accessToken && !GlobalConfig.needRefreshToken(this.#accessToken))
|
|
302
|
+
this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
|
|
303
|
+
}
|
|
304
|
+
#authorize(accessTokenDto) {
|
|
305
|
+
this.#accessToken = GlobalConfig.toAccessToken(accessTokenDto);
|
|
306
|
+
this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
|
|
307
|
+
return this.#accessToken;
|
|
308
|
+
}
|
|
309
|
+
async uploadEnv(devProjectId, file) {
|
|
310
|
+
const formData = new FormData;
|
|
311
|
+
formData.append("devProjectId", devProjectId);
|
|
312
|
+
formData.append("file", file);
|
|
313
|
+
return await this.#api.post(`/uploadEnv/${devProjectId}`, formData);
|
|
314
|
+
}
|
|
315
|
+
async uploadSecret(devProjectId, secret) {
|
|
316
|
+
return await this.#api.post(`/uploadSecret/${encodeURIComponent(devProjectId)}`, secret);
|
|
317
|
+
}
|
|
318
|
+
async listSecrets(devProjectId) {
|
|
319
|
+
return await this.#api.get(`/listSecrets/${encodeURIComponent(devProjectId)}`);
|
|
320
|
+
}
|
|
321
|
+
async removeSecret(devProjectId, secret) {
|
|
322
|
+
return await this.#api.post(`/removeSecret/${encodeURIComponent(devProjectId)}`, secret);
|
|
323
|
+
}
|
|
324
|
+
async requestUpdateUpload(devProjectId, upload) {
|
|
325
|
+
return await this.#api.post(`/requestUpdateUpload/${encodeURIComponent(devProjectId)}`, upload);
|
|
326
|
+
}
|
|
327
|
+
async commitUpdateUpload(devProjectId, uploadId) {
|
|
328
|
+
return await this.#api.post(`/commitUpdateUpload/${encodeURIComponent(devProjectId)}`, { uploadId }, { signal: AbortSignal.timeout(11 * 60000) });
|
|
329
|
+
}
|
|
330
|
+
async registerAppBinary(devProjectId, binary) {
|
|
331
|
+
const formData = new FormData;
|
|
332
|
+
for (const key of ["appName", "target", "distribution"])
|
|
333
|
+
formData.append(key, binary[key]);
|
|
334
|
+
if (binary.channel)
|
|
335
|
+
formData.append("channel", binary.channel);
|
|
336
|
+
formData.append("bundle", binary.bundle, "bundle.json");
|
|
337
|
+
formData.append("packUploadId", binary.packUploadId);
|
|
338
|
+
return await this.#api.post(`/registerAppBinary/${encodeURIComponent(devProjectId)}`, formData, {
|
|
339
|
+
signal: AbortSignal.timeout(11 * 60000)
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async downloadEnv(devProjectId) {
|
|
343
|
+
const localPath = `${this.#workspace.workspaceRoot}/local/env.tar`;
|
|
344
|
+
await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
|
|
345
|
+
return localPath;
|
|
346
|
+
}
|
|
347
|
+
async getRemoteAuthToken(remoteId) {
|
|
348
|
+
try {
|
|
349
|
+
return this.#authorize(await this.#api.get(`/getRemoteAuthToken/${remoteId}`));
|
|
350
|
+
} catch (_) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async refreshAuthToken(refreshToken) {
|
|
355
|
+
return this.#authorize(await this.#api.post(`/refreshAuthToken`, { refreshToken }, { signal: AbortSignal.timeout(20000) }));
|
|
356
|
+
}
|
|
357
|
+
async requestTunnel(input) {
|
|
358
|
+
return await this.#api.post(`/tunnel/requestTunnel`, input);
|
|
359
|
+
}
|
|
360
|
+
async tunnelListInSelf() {
|
|
361
|
+
return await this.#api.get(`/tunnel/tunnelListInSelf`);
|
|
362
|
+
}
|
|
363
|
+
async revokeTunnel(code) {
|
|
364
|
+
return await this.#api.post(`/tunnel/revokeTunnel`, { code }, { signal: AbortSignal.timeout(5000) });
|
|
365
|
+
}
|
|
366
|
+
async getRemoteSelf() {
|
|
367
|
+
try {
|
|
368
|
+
return await this.#api.get(`/getRemoteSelf`);
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// pkgs/@akanjs/devkit/cloud/cloudUpdates.ts
|
|
375
|
+
import path2 from "path";
|
|
376
|
+
import { Logger as Logger2 } from "akanjs/common";
|
|
377
|
+
|
|
378
|
+
// pkgs/@akanjs/devkit/cloud/updatePackUpload.ts
|
|
379
|
+
import { Logger, sleep } from "akanjs/common";
|
|
380
|
+
class UpdatePackUpload {
|
|
381
|
+
static maxBytes = 200 * 1024 * 1024;
|
|
382
|
+
static #transientPut = new Set([0, 408, 429, 500, 502, 503, 504]);
|
|
383
|
+
static #putFloorBytesPerSecond = 64 * 1024;
|
|
384
|
+
static #maxPutRetryAfterMs = 60000;
|
|
385
|
+
#api;
|
|
386
|
+
#devProjectId;
|
|
387
|
+
#putRetryMs;
|
|
388
|
+
#putTimeoutMs;
|
|
389
|
+
#commitRetryMs;
|
|
390
|
+
#cutOffWindowMs;
|
|
391
|
+
#commitLimitMs;
|
|
392
|
+
constructor(api, devProjectId, {
|
|
393
|
+
putRetryMs = 1000,
|
|
394
|
+
putTimeoutMs = 60000,
|
|
395
|
+
commitRetryMs = 1e4,
|
|
396
|
+
cutOffWindowMs = 15 * 60000,
|
|
397
|
+
commitLimitMs = 60 * 60000
|
|
398
|
+
} = {}) {
|
|
399
|
+
this.#api = api;
|
|
400
|
+
this.#devProjectId = devProjectId;
|
|
401
|
+
this.#putRetryMs = putRetryMs;
|
|
402
|
+
this.#putTimeoutMs = putTimeoutMs;
|
|
403
|
+
this.#commitRetryMs = commitRetryMs;
|
|
404
|
+
this.#cutOffWindowMs = cutOffWindowMs;
|
|
405
|
+
this.#commitLimitMs = commitLimitMs;
|
|
406
|
+
}
|
|
407
|
+
async send(archive, fields) {
|
|
408
|
+
const uploadId = await this.stage(archive, fields);
|
|
409
|
+
return await this.settle(() => this.#api.commitUpdateUpload(this.#devProjectId, uploadId));
|
|
410
|
+
}
|
|
411
|
+
async stage(archive, fields) {
|
|
412
|
+
const file = Bun.file(archive);
|
|
413
|
+
if (file.size < 1 || file.size > UpdatePackUpload.maxBytes)
|
|
414
|
+
throw new Error(`The pack archive is ${file.size} bytes; a project takes 1 byte to 200 MiB.`);
|
|
415
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
416
|
+
for await (const chunk of file.stream())
|
|
417
|
+
hasher.update(chunk);
|
|
418
|
+
const upload = { ...fields, size: file.size, sha256: hasher.digest("hex") };
|
|
419
|
+
for (let ticket = 1;; ticket++) {
|
|
420
|
+
const { uploadId, uploadUrl, contentType } = await this.#request(upload);
|
|
421
|
+
const status = await this.#put(uploadUrl, contentType, file);
|
|
422
|
+
if (status >= 200 && status < 300)
|
|
423
|
+
return uploadId;
|
|
424
|
+
if (status !== 403 || ticket === 2)
|
|
425
|
+
throw new Error(`The storage refused the pack upload (${status ? `HTTP ${status}` : "no response"}).`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async#request(upload) {
|
|
429
|
+
return await this.#api.requestUpdateUpload(this.#devProjectId, upload).catch((error) => {
|
|
430
|
+
if (error instanceof CloudHttpError && error.status === 404 && !error.errKey)
|
|
431
|
+
throw new Error("The project's server does not take direct pack uploads yet: update Akan Cloud and try again, or upload the pack from the project's release screen.");
|
|
432
|
+
throw error;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async#put(url, contentType, file) {
|
|
436
|
+
const timeoutMs = this.#putTimeoutMs + Math.ceil(file.size / UpdatePackUpload.#putFloorBytesPerSecond * 1000);
|
|
437
|
+
for (let retry = 0;; retry++) {
|
|
438
|
+
const { status, retryAfterMs } = await fetch(url, {
|
|
439
|
+
method: "PUT",
|
|
440
|
+
body: file,
|
|
441
|
+
headers: { "Content-Type": contentType },
|
|
442
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
443
|
+
}).then(async (response) => {
|
|
444
|
+
await response.arrayBuffer().catch(() => {
|
|
445
|
+
return;
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
status: response.status,
|
|
449
|
+
retryAfterMs: CloudHttpError.retryAfterMs(response.headers.get("retry-after"))
|
|
450
|
+
};
|
|
451
|
+
}).catch(() => ({ status: 0, retryAfterMs: null }));
|
|
452
|
+
if (retry === 3 || !UpdatePackUpload.#transientPut.has(status))
|
|
453
|
+
return status;
|
|
454
|
+
await sleep(Math.max(this.#putRetryMs * (retry + 1), Math.min(retryAfterMs ?? 0, UpdatePackUpload.#maxPutRetryAfterMs)));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async settle(call) {
|
|
458
|
+
const limit = performance.now() + this.#commitLimitMs;
|
|
459
|
+
let cutOffSince = null;
|
|
460
|
+
for (;; ) {
|
|
461
|
+
try {
|
|
462
|
+
return await call();
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const now = performance.now();
|
|
465
|
+
if (UpdatePackUpload.#inProgress(error))
|
|
466
|
+
cutOffSince = null;
|
|
467
|
+
else if (UpdatePackUpload.#cutOff(error))
|
|
468
|
+
cutOffSince ??= now;
|
|
469
|
+
else
|
|
470
|
+
throw error;
|
|
471
|
+
const deadline = cutOffSince === null ? limit : Math.min(limit, cutOffSince + this.#cutOffWindowMs);
|
|
472
|
+
const waitMs = Math.max(this.#commitRetryMs, error instanceof CloudHttpError && error.retryAfterMs || 0);
|
|
473
|
+
if (now + waitMs > deadline)
|
|
474
|
+
throw error;
|
|
475
|
+
Logger.rawLog(`The project is still taking the pack; asking again in ${Math.round(waitMs / 1000)}s.`);
|
|
476
|
+
await sleep(waitMs);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
static #inProgress(error) {
|
|
481
|
+
return error instanceof CloudHttpError && error.status === 409 && !!error.errKey?.endsWith(".uploadCommitInProgress");
|
|
482
|
+
}
|
|
483
|
+
static #cutOff(error) {
|
|
484
|
+
if (!(error instanceof CloudHttpError))
|
|
485
|
+
return !(error instanceof SyntaxError);
|
|
486
|
+
const { status, errKey } = error;
|
|
487
|
+
if (status === 503)
|
|
488
|
+
return errKey === null || errKey === "base.error.serverUnavailable";
|
|
489
|
+
return status === 502 || status === 504 || status >= 520 && status <= 527;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// pkgs/@akanjs/devkit/cloud/cloudUpdates.ts
|
|
494
|
+
class CloudUpdates {
|
|
495
|
+
cloudApi;
|
|
496
|
+
devProjectId;
|
|
497
|
+
constructor(cloudApi, devProjectId) {
|
|
498
|
+
this.cloudApi = cloudApi;
|
|
499
|
+
this.devProjectId = devProjectId;
|
|
500
|
+
}
|
|
501
|
+
static async storeBundlePath(app, target, platform) {
|
|
502
|
+
const [mobileTarget] = await resolveMobileTargets(app, target);
|
|
503
|
+
if (!mobileTarget)
|
|
504
|
+
throw new Error(`Native target '${target}' was not found.`);
|
|
505
|
+
return path2.join(new NativeApp(app, mobileTarget).outDir(platform), UpdatePackFolder.bundleFile);
|
|
506
|
+
}
|
|
507
|
+
static async readPack(app, dir, {
|
|
508
|
+
platform,
|
|
509
|
+
target,
|
|
510
|
+
env,
|
|
511
|
+
allowLocalRelease
|
|
512
|
+
}) {
|
|
513
|
+
const pack = await UpdatePackFolder.read(dir);
|
|
514
|
+
const what = `The pack in ${pack.out} is ${pack.app} ${pack.target} ${pack.platform} --env ${pack.env}`;
|
|
515
|
+
if (pack.app !== app.name)
|
|
516
|
+
throw new Error(`${what}, not ${app.name}'s.`);
|
|
517
|
+
if (platform && platform !== pack.platform)
|
|
518
|
+
throw new Error(`${what}, not ${platform}.`);
|
|
519
|
+
if (env && env !== pack.env)
|
|
520
|
+
throw new Error(`${what}, not --env ${env}.`);
|
|
521
|
+
if (target) {
|
|
522
|
+
const targets = (await resolveMobileTargets(app, target)).map(({ name }) => name);
|
|
523
|
+
if (targets.length !== 1 || targets[0] !== pack.target)
|
|
524
|
+
throw new Error(`${what}, not --target ${target} (${targets.join(", ")}).`);
|
|
525
|
+
}
|
|
526
|
+
if (pack.env === "local" && !allowLocalRelease)
|
|
527
|
+
throw new Error(`${what}, and a local pack is blocked. Pass --allow-local-release only for explicit local release testing.`);
|
|
528
|
+
return pack;
|
|
529
|
+
}
|
|
530
|
+
async push(pack, { channel, notes }) {
|
|
531
|
+
const target = channel ?? pack.channel;
|
|
532
|
+
return await UpdatePackFolder.withArchive(pack, async (archive) => {
|
|
533
|
+
const release = await new UpdatePackUpload(this.cloudApi, this.devProjectId).send(archive, { appName: pack.app, target: pack.target, platform: pack.platform, channel: target, notes }).catch((error) => {
|
|
534
|
+
throw CloudApi.refusal(error, "a pack is uploaded by a project member signed in with `akan login`");
|
|
535
|
+
});
|
|
536
|
+
const label = `${pack.app} ${pack.target} ${pack.platform} on channel ${target}: release ${release.id}`;
|
|
537
|
+
if (release.status === "rejected")
|
|
538
|
+
throw new ExitCodeError(`The project rejected ${label}:
|
|
539
|
+
${(release.problems ?? []).map((problem) => ` - ${problem}`).join(`
|
|
540
|
+
`)}`, 2);
|
|
541
|
+
if (release.status === "published")
|
|
542
|
+
Logger2.rawLog(`Published ${label} (${pack.files} files, native API ${pack.nativeApi}).`);
|
|
543
|
+
else
|
|
544
|
+
Logger2.rawLog(`Staged ${label} (${pack.files} files, native API ${pack.nativeApi}). Publish it from the project's release screen.`);
|
|
545
|
+
return release;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async registerBinary(app, pack, { distribution, env }) {
|
|
549
|
+
const { cloudApi, devProjectId } = this;
|
|
550
|
+
const bundlePath = await CloudUpdates.storeBundlePath(app, pack.target, pack.platform);
|
|
551
|
+
const bundleFile = Bun.file(bundlePath);
|
|
552
|
+
const rebuild = `akan release-${pack.platform} ${app.name} --target ${pack.target} --env ${env}`;
|
|
553
|
+
if (!await bundleFile.exists())
|
|
554
|
+
throw new Error(`No store build at ${bundlePath}: run \`${rebuild}\` first.`);
|
|
555
|
+
const bundle = await UpdatePackFolder.readBundle(bundlePath);
|
|
556
|
+
if (!bundle.updates)
|
|
557
|
+
throw new Error(`The store build at ${bundlePath} takes no updates: give the target native.updates.`);
|
|
558
|
+
const packBundle = path2.join(pack.out, UpdatePackFolder.bundleFile);
|
|
559
|
+
const envProblem = await NativePublicEnv.problemAgainst(bundlePath, packBundle);
|
|
560
|
+
if (envProblem)
|
|
561
|
+
throw new ExitCodeError(`The pack in ${pack.out} cannot run in the store build: ${envProblem}.`, 2);
|
|
562
|
+
if (bundle.web.hash !== pack.webHash)
|
|
563
|
+
throw new Error(`The store build at ${bundlePath} is not this commit's --env ${env} build: its web bundle differs from the pack's. Run \`${rebuild}\`, then register that build.`);
|
|
564
|
+
const compat = await NativeApp.compareBundles(app.cwdPath, bundlePath, packBundle);
|
|
565
|
+
const verdict = UpdatePackFolder.storeBuildVerdict({ out: pack.out, compat: { against: bundlePath, ...compat } }, `Run \`${rebuild}\`, then register that build.`);
|
|
566
|
+
if (verdict)
|
|
567
|
+
throw verdict;
|
|
568
|
+
if (bundle.updates.channel !== pack.channel)
|
|
569
|
+
throw new Error(`The store build at ${bundlePath} reads updates channel ${bundle.updates.channel}, and this commit's --env ${env} pack is for ${pack.channel}. Run \`${rebuild}\`, then register that build.`);
|
|
570
|
+
const { channel } = pack;
|
|
571
|
+
const upload = new UpdatePackUpload(cloudApi, devProjectId);
|
|
572
|
+
return await UpdatePackFolder.withArchive(pack, async (archive) => {
|
|
573
|
+
const binary = await upload.stage(archive, { appName: pack.app, target: pack.target, platform: pack.platform, channel, notes: null }).then((packUploadId) => upload.settle(() => cloudApi.registerAppBinary(devProjectId, {
|
|
574
|
+
appName: pack.app,
|
|
575
|
+
target: pack.target,
|
|
576
|
+
distribution,
|
|
577
|
+
bundle: bundleFile,
|
|
578
|
+
packUploadId,
|
|
579
|
+
channel
|
|
580
|
+
}))).catch((error) => {
|
|
581
|
+
throw CloudApi.refusal(error, "a binary is registered by a project member signed in with `akan login`");
|
|
582
|
+
});
|
|
583
|
+
Logger2.rawLog(`Registered ${pack.app} ${pack.target} ${pack.platform} (${distribution}): binary ${binary.id}, native API ${pack.nativeApi}.`);
|
|
584
|
+
return binary;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// pkgs/@akanjs/devkit/cloud/pipelineSecrets.ts
|
|
589
|
+
import { capitalize, Logger as Logger3, PipelineNames } from "akanjs/common";
|
|
590
|
+
class PipelineSecrets {
|
|
591
|
+
cloudApi;
|
|
592
|
+
devProjectId;
|
|
593
|
+
static #secretBytes = 64 * 1024;
|
|
594
|
+
static #secretBranches = ["debug", "develop", "main"];
|
|
595
|
+
static #secretsUnavailable = "The server is not configured for pipeline secrets yet";
|
|
596
|
+
constructor(cloudApi, devProjectId) {
|
|
597
|
+
this.cloudApi = cloudApi;
|
|
598
|
+
this.devProjectId = devProjectId;
|
|
599
|
+
}
|
|
600
|
+
static async readValue(name, file) {
|
|
601
|
+
const raw = file ? await Bun.file(file).text() : process.stdin.isTTY ? await (await import("@inquirer/prompts")).password({
|
|
602
|
+
message: `Value of ${name} (hidden; it can never be read back)`,
|
|
603
|
+
mask: "*"
|
|
604
|
+
}) : await PipelineSecrets.#readStdin(name);
|
|
605
|
+
const value = raw.replace(/\r?\n$/, "");
|
|
606
|
+
if (!value)
|
|
607
|
+
throw new Error(`The value of ${name} is empty.`);
|
|
608
|
+
const bytes = new TextEncoder().encode(value).length;
|
|
609
|
+
if (bytes > PipelineSecrets.#secretBytes)
|
|
610
|
+
throw new Error(`The value of ${name} is ${bytes} bytes, over ${PipelineSecrets.#secretBytes}.`);
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
static async#readStdin(name) {
|
|
614
|
+
process.stderr.write(`Reading the value of ${name} from stdin until it closes (or pass --file <path>).
|
|
615
|
+
`);
|
|
616
|
+
return await Bun.stdin.text();
|
|
617
|
+
}
|
|
618
|
+
async upload({ name, branch, value }) {
|
|
619
|
+
PipelineSecrets.assertName(name);
|
|
620
|
+
PipelineSecrets.assertBranch(branch);
|
|
621
|
+
const saved = await this.cloudApi.uploadSecret(this.devProjectId, { scope: branch ?? "*", name, value }).catch((error) => {
|
|
622
|
+
throw CloudApi.refusal(error, "a secret is written by a project member signed in with `akan login`", {
|
|
623
|
+
unavailable: PipelineSecrets.#secretsUnavailable
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
Logger3.rawLog(`Saved ${saved.name} for ${saved.scope === "*" ? "every branch" : `branch ${saved.scope}`}. Builds that start from now on use it.`);
|
|
627
|
+
}
|
|
628
|
+
async list(branch) {
|
|
629
|
+
PipelineSecrets.assertBranch(branch);
|
|
630
|
+
const listed = await this.cloudApi.listSecrets(this.devProjectId).catch((error) => {
|
|
631
|
+
throw CloudApi.refusal(error, "the secrets are listed for a project member", {
|
|
632
|
+
unavailable: PipelineSecrets.#secretsUnavailable
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
const secrets = listed.filter((secret) => !branch || secret.scope === "*" || secret.scope === branch);
|
|
636
|
+
Logger3.rawLog(PipelineSecrets.format(secrets));
|
|
637
|
+
}
|
|
638
|
+
async remove({ name, branch }) {
|
|
639
|
+
PipelineSecrets.assertName(name);
|
|
640
|
+
PipelineSecrets.assertBranch(branch);
|
|
641
|
+
const removed = await this.cloudApi.removeSecret(this.devProjectId, { scope: branch ?? "*", name }).catch((error) => {
|
|
642
|
+
throw CloudApi.refusal(error, "a secret is removed by a project member signed in with `akan login`", {
|
|
643
|
+
unavailable: PipelineSecrets.#secretsUnavailable
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
Logger3.rawLog(removed ? `Removed ${name} for ${branch ? `branch ${branch}` : "every branch"}.` : `No ${name} for ${branch ? `branch ${branch}` : "every branch"} to remove.`);
|
|
647
|
+
}
|
|
648
|
+
static format(secrets) {
|
|
649
|
+
if (!secrets.length)
|
|
650
|
+
return "No pipeline secrets.";
|
|
651
|
+
const rows = [...secrets].sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope)).map((secret) => [secret.name, secret.scope === "*" ? "every branch" : secret.scope, secret.updatedAt, secret.updatedBy].map((cell) => String(cell ?? "-")));
|
|
652
|
+
const header = ["NAME", "SCOPE", "UPDATED", "BY"];
|
|
653
|
+
const widths = header.map((title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
654
|
+
return [header, ...rows].map((row) => row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()).join(`
|
|
655
|
+
`);
|
|
656
|
+
}
|
|
657
|
+
static assertName(name) {
|
|
658
|
+
const problem = PipelineNames.secretNameError(name);
|
|
659
|
+
if (problem)
|
|
660
|
+
throw new Error(`${capitalize(problem)}.`);
|
|
661
|
+
}
|
|
662
|
+
static assertBranch(branch) {
|
|
663
|
+
if (branch !== null && !PipelineSecrets.#secretBranches.includes(branch))
|
|
664
|
+
throw new Error(`--branch ${branch} is not a deployed branch: ${PipelineSecrets.#secretBranches.join(", ")}.`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
export { getDefaultHostConfig, GlobalConfig, CloudApi, CloudUpdates, PipelineSecrets };
|