@dac-tech-team-tw/itsaplan-cli 0.1.0 → 0.3.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 +94 -49
- package/dist/cli.js +269 -89
- package/dist/commands.js +192 -25
- package/dist/config.d.ts +9 -0
- package/dist/config.js +62 -2
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/initiatives.d.ts +16 -0
- package/dist/initiatives.js +241 -0
- package/dist/issues.d.ts +2 -0
- package/dist/issues.js +97 -6
- package/dist/output.d.ts +12 -0
- package/dist/output.js +261 -0
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.js +56 -0
- package/dist/writes.d.ts +3 -3
- package/dist/writes.js +6 -6
- package/package.json +2 -1
package/dist/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { access, lstat, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { input, password } from "@inquirer/prompts";
|
|
4
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
5
5
|
import { stringify } from "smol-toml";
|
|
6
6
|
import { API_BASELINE, CLI_VERSION, CliError, } from "./contracts.js";
|
|
7
7
|
import { createContext, getConfigPaths, normalizeApiUrl, readUserConfig, removeAuth, resolveApiUrl, saveAuth, } from "./config.js";
|
|
@@ -247,26 +247,142 @@ export async function runDoctor(options) {
|
|
|
247
247
|
}
|
|
248
248
|
return { data: diagnostics, meta: { apiVersion: diagnostics.serverVersion } };
|
|
249
249
|
}
|
|
250
|
+
function environmentAuthWarning() {
|
|
251
|
+
const endpoint = process.env.ITSAPLAN_URL
|
|
252
|
+
? "後續仍使用 ITSAPLAN_URL 指向的環境憑證,身份可能與已存登入不同。"
|
|
253
|
+
: "缺少 ITSAPLAN_URL,後續未帶 --api-url 的指令仍會失敗(EXPLICIT_ENDPOINT_REQUIRED)。";
|
|
254
|
+
const remedy = process.platform === "win32"
|
|
255
|
+
? "如要使用已存登入,請在目前 shell 移除 ITSAPLAN_API_KEY 與 ITSAPLAN_URL,再執行 iap auth status。"
|
|
256
|
+
: "如要暫時使用已存登入:env -u ITSAPLAN_API_KEY -u ITSAPLAN_URL iap auth status";
|
|
257
|
+
return `ITSAPLAN_API_KEY 仍優先於已儲存的登入。\n${endpoint}\n${remedy}`;
|
|
258
|
+
}
|
|
259
|
+
async function loginEffectiveAuth(apiUrl, user) {
|
|
260
|
+
const effective = {
|
|
261
|
+
source: process.env.ITSAPLAN_API_KEY !== undefined ? "env" : "unresolved",
|
|
262
|
+
apiUrl: null,
|
|
263
|
+
tokenAvailable: process.env.ITSAPLAN_API_KEY !== undefined
|
|
264
|
+
? Boolean(process.env.ITSAPLAN_API_KEY)
|
|
265
|
+
: null,
|
|
266
|
+
authenticated: false,
|
|
267
|
+
usable: null,
|
|
268
|
+
verification: "not_run",
|
|
269
|
+
};
|
|
270
|
+
try {
|
|
271
|
+
// Describe the next unqualified command, not the flags used for this login.
|
|
272
|
+
if (process.env.ITSAPLAN_URL !== undefined)
|
|
273
|
+
effective.apiUrl = normalizeApiUrl(process.env.ITSAPLAN_URL);
|
|
274
|
+
const ctx = await createContext({});
|
|
275
|
+
Object.assign(effective, ctx.auth);
|
|
276
|
+
if (!ctx.auth.tokenAvailable) {
|
|
277
|
+
effective.usable = false;
|
|
278
|
+
effective.error = errorInfo(new CliError("AUTHENTICATION_REQUIRED", "目前生效的 API 服務網址沒有可用的 API key。", 3));
|
|
279
|
+
}
|
|
280
|
+
else if (ctx.auth.source === "config" && ctx.auth.apiUrl === apiUrl) {
|
|
281
|
+
Object.assign(effective, {
|
|
282
|
+
authenticated: true,
|
|
283
|
+
usable: true,
|
|
284
|
+
verification: "passed",
|
|
285
|
+
user,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
effective.usable = false;
|
|
291
|
+
effective.error = errorInfo(error);
|
|
292
|
+
}
|
|
293
|
+
return effective;
|
|
294
|
+
}
|
|
250
295
|
export async function runAuth(action, options) {
|
|
251
296
|
try {
|
|
252
297
|
if (action === "status") {
|
|
253
|
-
const
|
|
298
|
+
const config = await readUserConfig();
|
|
299
|
+
const storage = config.api_url
|
|
300
|
+
? config.credentials?.[config.api_url]?.storage
|
|
301
|
+
: undefined;
|
|
302
|
+
const saved = {
|
|
303
|
+
apiUrl: config.api_url ?? null,
|
|
304
|
+
configured: storage !== undefined,
|
|
305
|
+
storage: storage ?? null,
|
|
306
|
+
tokenAvailable: null,
|
|
307
|
+
authenticated: false,
|
|
308
|
+
verification: "not_run",
|
|
309
|
+
credentials: Object.entries(config.credentials ?? {}).map(([apiUrl, entry]) => ({ apiUrl, storage: entry.storage })),
|
|
310
|
+
};
|
|
311
|
+
const envDefined = process.env.ITSAPLAN_API_KEY !== undefined;
|
|
312
|
+
const effective = {
|
|
313
|
+
source: envDefined
|
|
314
|
+
? "env"
|
|
315
|
+
: options.apiKey !== undefined
|
|
316
|
+
? "flag"
|
|
317
|
+
: storage
|
|
318
|
+
? "config"
|
|
319
|
+
: "missing",
|
|
320
|
+
apiUrl: null,
|
|
321
|
+
tokenAvailable: envDefined
|
|
322
|
+
? Boolean(process.env.ITSAPLAN_API_KEY)
|
|
323
|
+
: options.apiKey !== undefined
|
|
324
|
+
? Boolean(options.apiKey)
|
|
325
|
+
: null,
|
|
326
|
+
authenticated: false,
|
|
327
|
+
usable: false,
|
|
328
|
+
verification: "not_run",
|
|
329
|
+
};
|
|
254
330
|
const status = {
|
|
255
|
-
|
|
331
|
+
source: effective.source,
|
|
332
|
+
apiUrl: null,
|
|
333
|
+
tokenAvailable: effective.tokenAvailable,
|
|
256
334
|
authenticated: false,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
: {}),
|
|
335
|
+
saved,
|
|
336
|
+
effective,
|
|
337
|
+
...(envDefined ? { warning: environmentAuthWarning() } : {}),
|
|
262
338
|
};
|
|
339
|
+
let checkingSaved = false;
|
|
340
|
+
let verificationStarted = false;
|
|
263
341
|
try {
|
|
342
|
+
const explicitUrl = options.apiUrl ?? process.env.ITSAPLAN_URL;
|
|
343
|
+
if (explicitUrl !== undefined)
|
|
344
|
+
effective.apiUrl = normalizeApiUrl(explicitUrl);
|
|
345
|
+
else if (!envDefined && options.apiKey === undefined)
|
|
346
|
+
effective.apiUrl = config.api_url ?? null;
|
|
347
|
+
const ctx = await createContext(options);
|
|
348
|
+
Object.assign(effective, ctx.auth);
|
|
349
|
+
Object.assign(status, ctx.auth);
|
|
350
|
+
checkingSaved =
|
|
351
|
+
ctx.auth.source === "config" && ctx.auth.apiUrl === saved.apiUrl;
|
|
352
|
+
if (!envDefined &&
|
|
353
|
+
ctx.auth.apiUrl === saved.apiUrl &&
|
|
354
|
+
ctx.auth.storage === saved.storage)
|
|
355
|
+
saved.tokenAvailable = ctx.auth.tokenAvailable;
|
|
264
356
|
if (!ctx.auth.tokenAvailable)
|
|
265
|
-
throw new CliError("AUTHENTICATION_REQUIRED", "
|
|
357
|
+
throw new CliError("AUTHENTICATION_REQUIRED", "沒有可用的 API key;請執行 auth login,或為 ITSAPLAN_API_KEY 明確指定 API 服務網址。", 3);
|
|
358
|
+
verificationStarted = true;
|
|
266
359
|
const user = identity(await ctx.client.request("/me"));
|
|
360
|
+
Object.assign(effective, {
|
|
361
|
+
authenticated: true,
|
|
362
|
+
usable: true,
|
|
363
|
+
verification: "passed",
|
|
364
|
+
user,
|
|
365
|
+
});
|
|
366
|
+
if (checkingSaved)
|
|
367
|
+
Object.assign(saved, {
|
|
368
|
+
authenticated: true,
|
|
369
|
+
verification: "passed",
|
|
370
|
+
user,
|
|
371
|
+
});
|
|
267
372
|
return { data: { ...status, authenticated: true, user } };
|
|
268
373
|
}
|
|
269
374
|
catch (error) {
|
|
375
|
+
effective.error = errorInfo(error);
|
|
376
|
+
if (verificationStarted) {
|
|
377
|
+
effective.verification = "failed";
|
|
378
|
+
if (checkingSaved)
|
|
379
|
+
saved.verification = "failed";
|
|
380
|
+
}
|
|
381
|
+
Object.assign(status, {
|
|
382
|
+
source: effective.source,
|
|
383
|
+
apiUrl: effective.apiUrl,
|
|
384
|
+
tokenAvailable: effective.tokenAvailable,
|
|
385
|
+
});
|
|
270
386
|
return {
|
|
271
387
|
data: status,
|
|
272
388
|
error: errorInfo(error),
|
|
@@ -277,7 +393,7 @@ export async function runAuth(action, options) {
|
|
|
277
393
|
if (action === "logout") {
|
|
278
394
|
const apiUrl = await resolveApiUrl(options);
|
|
279
395
|
if (!apiUrl)
|
|
280
|
-
throw new CliError("MISSING_API_URL", "
|
|
396
|
+
throw new CliError("MISSING_API_URL", "請以 --api-url 或 ITSAPLAN_URL 指定 API 服務網址。", 2);
|
|
281
397
|
const removed = await removeAuth(apiUrl);
|
|
282
398
|
return {
|
|
283
399
|
data: {
|
|
@@ -286,34 +402,70 @@ export async function runAuth(action, options) {
|
|
|
286
402
|
...removed,
|
|
287
403
|
serverRevoked: false,
|
|
288
404
|
environmentKeyStillActive: Boolean(process.env.ITSAPLAN_API_KEY),
|
|
289
|
-
warning: "
|
|
405
|
+
warning: "登出只移除本機憑證,不會撤銷伺服器 key 或清除環境變數。",
|
|
290
406
|
},
|
|
291
407
|
outcome: "applied",
|
|
292
408
|
verification: "passed",
|
|
293
409
|
};
|
|
294
410
|
}
|
|
295
411
|
if (action !== "login")
|
|
296
|
-
throw new CliError("INVALID_ACTION", "
|
|
412
|
+
throw new CliError("INVALID_ACTION", "請使用 auth login、status 或 logout。", 2);
|
|
297
413
|
if (options.storage !== undefined &&
|
|
298
414
|
options.storage !== "file" &&
|
|
299
415
|
options.storage !== "keyring")
|
|
300
|
-
throw new CliError("INVALID_STORAGE", "
|
|
416
|
+
throw new CliError("INVALID_STORAGE", "請選擇 --storage file 或 keyring。", 2);
|
|
301
417
|
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
302
418
|
if (!options.withToken && !interactive)
|
|
303
|
-
throw new CliError("INTERACTIVE_LOGIN_REQUIRED", "
|
|
419
|
+
throw new CliError("INTERACTIVE_LOGIN_REQUIRED", "非互動登入須使用 --with-token,並指定可信的 API 服務網址。", 2);
|
|
304
420
|
let apiUrl = await resolveApiUrl(options);
|
|
305
421
|
if (!options.withToken && interactive) {
|
|
306
|
-
const
|
|
422
|
+
const source = options.apiUrl !== undefined
|
|
423
|
+
? "--api-url"
|
|
424
|
+
: process.env.ITSAPLAN_URL !== undefined
|
|
425
|
+
? "ITSAPLAN_URL"
|
|
426
|
+
: "已儲存設定";
|
|
427
|
+
const answer = await input({
|
|
428
|
+
message: `API 服務網址(API 部署網域,非網頁網址;含 https://${apiUrl ? `;預設來自 ${source}` : ""}):`,
|
|
429
|
+
...(apiUrl ? { default: apiUrl } : {}),
|
|
430
|
+
}, { output: process.stderr });
|
|
307
431
|
apiUrl = normalizeApiUrl(answer);
|
|
308
432
|
}
|
|
309
433
|
if (!apiUrl)
|
|
310
|
-
throw new CliError("MISSING_API_URL", "
|
|
434
|
+
throw new CliError("MISSING_API_URL", "非互動登入須以 --api-url 或 ITSAPLAN_URL 指定 API 服務網址。", 2);
|
|
435
|
+
if (!options.withToken &&
|
|
436
|
+
interactive &&
|
|
437
|
+
process.env.ITSAPLAN_API_KEY !== undefined) {
|
|
438
|
+
const proceed = await select({
|
|
439
|
+
message: environmentAuthWarning(),
|
|
440
|
+
choices: [
|
|
441
|
+
{
|
|
442
|
+
name: "取消,不讀取或儲存新的 key",
|
|
443
|
+
value: false,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "繼續儲存,但不改變環境憑證的優先序",
|
|
447
|
+
value: true,
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
default: false,
|
|
451
|
+
}, { output: process.stderr });
|
|
452
|
+
if (!proceed)
|
|
453
|
+
throw new CliError("LOGIN_CANCELLED", "已取消登入;未讀取或儲存 key,也未變更環境設定。", 2);
|
|
454
|
+
}
|
|
311
455
|
const token = (options.withToken
|
|
312
456
|
? await stdinText()
|
|
313
|
-
: await password({ message: "
|
|
457
|
+
: await password({ message: "個人 API key:", mask: "*" }, { output: process.stderr })).trim();
|
|
314
458
|
if (!token || /\s/.test(token))
|
|
315
|
-
throw new CliError("INVALID_TOKEN", "
|
|
316
|
-
|
|
459
|
+
throw new CliError("INVALID_TOKEN", "請提供一組非空白且不含空白字元的 API key。", 2);
|
|
460
|
+
let user;
|
|
461
|
+
try {
|
|
462
|
+
user = identity(await new HttpClient(apiUrl, token).request("/me"));
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (error instanceof CliError && error.code === "REDIRECT_REJECTED")
|
|
466
|
+
throw new CliError(error.code, "此網址回傳重新導向,可能是網頁登入網址,而非 API 服務網址。CLI 不會跟隨重新導向,key 也未轉送。請向管理者確認 API 部署網域,再以 iap auth login --api-url <API_URL> 重試。這不代表 key 無效;同一網頁網址即使手動輸入仍會被拒絕。", error.exitCode, error.details);
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
317
469
|
const storage = options.storage === "file" ? "file" : "keyring";
|
|
318
470
|
await saveAuth(apiUrl, token, storage);
|
|
319
471
|
return {
|
|
@@ -323,10 +475,18 @@ export async function runAuth(action, options) {
|
|
|
323
475
|
authenticated: true,
|
|
324
476
|
storage,
|
|
325
477
|
user,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
478
|
+
saved: {
|
|
479
|
+
apiUrl,
|
|
480
|
+
configured: true,
|
|
481
|
+
storage,
|
|
482
|
+
tokenAvailable: true,
|
|
483
|
+
authenticated: true,
|
|
484
|
+
verification: "passed",
|
|
485
|
+
user,
|
|
486
|
+
},
|
|
487
|
+
effective: await loginEffectiveAuth(apiUrl, user),
|
|
488
|
+
...(process.env.ITSAPLAN_API_KEY !== undefined
|
|
489
|
+
? { warning: environmentAuthWarning() }
|
|
330
490
|
: {}),
|
|
331
491
|
},
|
|
332
492
|
outcome: "applied",
|
|
@@ -429,6 +589,9 @@ function protectedResourceWrite(path) {
|
|
|
429
589
|
return "issue";
|
|
430
590
|
if (/^\/projects\/[^/]+\/documents(?:\/|$)/i.test(pathname))
|
|
431
591
|
return "document";
|
|
592
|
+
if (/^\/(?:initiatives|initiative-attachments)(?:\/|$)/i.test(pathname) ||
|
|
593
|
+
/^\/projects\/[^/]+\/initiatives(?:\/|$)/i.test(pathname))
|
|
594
|
+
return "initiative";
|
|
432
595
|
return undefined;
|
|
433
596
|
}
|
|
434
597
|
export async function runApi(ctx, path, options) {
|
|
@@ -443,9 +606,13 @@ export async function runApi(ctx, path, options) {
|
|
|
443
606
|
if (resource)
|
|
444
607
|
throw new CliError(resource === "issue"
|
|
445
608
|
? "RAW_ISSUE_WRITE_FORBIDDEN"
|
|
446
|
-
:
|
|
609
|
+
: resource === "document"
|
|
610
|
+
? "RAW_DOCUMENT_WRITE_FORBIDDEN"
|
|
611
|
+
: "RAW_INITIATIVE_WRITE_FORBIDDEN", resource === "issue"
|
|
447
612
|
? "Raw writes to issues and their child resources are disabled; use supported issue commands. Other issue mutations are not yet supported."
|
|
448
|
-
:
|
|
613
|
+
: resource === "document"
|
|
614
|
+
? "Raw Docs writes are disabled; use supported document and issue link-document commands. Other Docs mutations are not yet supported."
|
|
615
|
+
: "Raw initiative writes are disabled; use supported initiative commands. Other initiative mutations are not yet supported.", 2);
|
|
449
616
|
if (!write && options.input !== undefined)
|
|
450
617
|
throw new CliError("INPUT_NOT_SUPPORTED", "GET and HEAD do not accept --input.", 2);
|
|
451
618
|
let body;
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface UserConfig {
|
|
|
5
5
|
credentials?: Record<string, {
|
|
6
6
|
storage: Storage;
|
|
7
7
|
}>;
|
|
8
|
+
project_defaults?: Record<string, string>;
|
|
8
9
|
}
|
|
9
10
|
export declare function getConfigPaths(): {
|
|
10
11
|
directory: string;
|
|
@@ -15,6 +16,14 @@ export declare function normalizeApiUrl(input: string): string;
|
|
|
15
16
|
export declare function readUserConfig(): Promise<UserConfig>;
|
|
16
17
|
export declare function resolveApiUrl(options: GlobalOptions): Promise<string | undefined>;
|
|
17
18
|
export declare function resolveProject(options: GlobalOptions): Promise<string | undefined>;
|
|
19
|
+
export interface ProjectSelection {
|
|
20
|
+
projectRef?: string;
|
|
21
|
+
source: "flag" | "env" | "directory" | "user" | "missing";
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveProjectSelection(options: GlobalOptions): Promise<ProjectSelection>;
|
|
24
|
+
export declare function saveUserProject(input: string, projectRef: string | undefined): Promise<{
|
|
25
|
+
changed: boolean;
|
|
26
|
+
}>;
|
|
18
27
|
export declare function saveAuth(input: string, token: string, storage?: Storage): Promise<void>;
|
|
19
28
|
export declare function removeAuth(input: string): Promise<{
|
|
20
29
|
removed: boolean;
|
package/dist/config.js
CHANGED
|
@@ -99,6 +99,20 @@ export async function readUserConfig() {
|
|
|
99
99
|
config.credentials[normalized] = { storage: entry.storage };
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
if (raw.project_defaults !== undefined) {
|
|
103
|
+
if (!object(raw.project_defaults))
|
|
104
|
+
throw new CliError("INVALID_CONFIG", "Project defaults must be a table.");
|
|
105
|
+
config.project_defaults = {};
|
|
106
|
+
for (const [url, ref] of Object.entries(raw.project_defaults)) {
|
|
107
|
+
if (typeof ref !== "string" ||
|
|
108
|
+
!/^[A-Za-z0-9_-]+\.[A-Za-z][A-Za-z0-9_-]*$/u.test(ref))
|
|
109
|
+
throw new CliError("INVALID_CONFIG", "Saved project defaults must be full team.KEY refs.");
|
|
110
|
+
const normalized = normalizeApiUrl(url);
|
|
111
|
+
if (Object.hasOwn(config.project_defaults, normalized))
|
|
112
|
+
throw new CliError("INVALID_CONFIG", "Duplicate normalized API project endpoints.");
|
|
113
|
+
config.project_defaults[normalized] = ref;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
102
116
|
return config;
|
|
103
117
|
}
|
|
104
118
|
export async function resolveApiUrl(options) {
|
|
@@ -119,6 +133,9 @@ async function exists(path) {
|
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
135
|
export async function resolveProject(options) {
|
|
136
|
+
return (await resolveProjectSelection(options)).projectRef;
|
|
137
|
+
}
|
|
138
|
+
export async function resolveProjectSelection(options) {
|
|
122
139
|
const cwd = process.cwd();
|
|
123
140
|
const home = resolve(homedir());
|
|
124
141
|
let boundary = cwd;
|
|
@@ -159,11 +176,25 @@ export async function resolveProject(options) {
|
|
|
159
176
|
break;
|
|
160
177
|
current = dirname(current);
|
|
161
178
|
}
|
|
162
|
-
|
|
179
|
+
let chosen = options.project ?? process.env.ITSAPLAN_PROJECT ?? project;
|
|
180
|
+
let source = options.project !== undefined
|
|
181
|
+
? "flag"
|
|
182
|
+
: process.env.ITSAPLAN_PROJECT !== undefined
|
|
183
|
+
? "env"
|
|
184
|
+
: project !== undefined
|
|
185
|
+
? "directory"
|
|
186
|
+
: "missing";
|
|
187
|
+
if (chosen === undefined) {
|
|
188
|
+
const apiUrl = await resolveApiUrl(options);
|
|
189
|
+
if (apiUrl !== undefined)
|
|
190
|
+
chosen = (await readUserConfig()).project_defaults?.[apiUrl];
|
|
191
|
+
if (chosen !== undefined)
|
|
192
|
+
source = "user";
|
|
193
|
+
}
|
|
163
194
|
if (chosen !== undefined &&
|
|
164
195
|
!/^(?:[A-Za-z0-9_-]+\.)?[A-Za-z][A-Za-z0-9_-]*$/u.test(chosen))
|
|
165
196
|
throw new CliError("INVALID_PROJECT", "Use a project key or team.KEY ref.");
|
|
166
|
-
return chosen;
|
|
197
|
+
return { projectRef: chosen, source };
|
|
167
198
|
}
|
|
168
199
|
async function windowsSid() {
|
|
169
200
|
try {
|
|
@@ -275,6 +306,35 @@ async function atomicWrite(path, content) {
|
|
|
275
306
|
await prepared.discard();
|
|
276
307
|
}
|
|
277
308
|
}
|
|
309
|
+
export async function saveUserProject(input, projectRef) {
|
|
310
|
+
const apiUrl = normalizeApiUrl(input);
|
|
311
|
+
if (projectRef !== undefined &&
|
|
312
|
+
!/^[A-Za-z0-9_-]+\.[A-Za-z][A-Za-z0-9_-]*$/u.test(projectRef))
|
|
313
|
+
throw new CliError("INVALID_PROJECT", "Use a full team.KEY project ref.");
|
|
314
|
+
const config = await readUserConfig();
|
|
315
|
+
if (config.project_defaults?.[apiUrl] === projectRef)
|
|
316
|
+
return { changed: false };
|
|
317
|
+
if (projectRef === undefined) {
|
|
318
|
+
delete config.project_defaults?.[apiUrl];
|
|
319
|
+
if (Object.keys(config.project_defaults ?? {}).length === 0)
|
|
320
|
+
delete config.project_defaults;
|
|
321
|
+
}
|
|
322
|
+
else
|
|
323
|
+
config.project_defaults = {
|
|
324
|
+
...config.project_defaults,
|
|
325
|
+
[apiUrl]: projectRef,
|
|
326
|
+
};
|
|
327
|
+
await prepareDirectory();
|
|
328
|
+
const prepared = await prepareAtomicWrite(getConfigPaths().config, stringify(config));
|
|
329
|
+
try {
|
|
330
|
+
await prepared.commit();
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
await prepared.discard().catch(() => { });
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
return { changed: true };
|
|
337
|
+
}
|
|
278
338
|
async function readCredentials() {
|
|
279
339
|
const { directory, credentials } = getConfigPaths();
|
|
280
340
|
if (!(await exists(credentials)))
|
package/dist/contracts.d.ts
CHANGED
package/dist/contracts.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Context, type Result } from "./contracts.js";
|
|
2
|
+
import { type IssueOptions } from "./issues.js";
|
|
3
|
+
export interface InitiativeOptions extends IssueOptions {
|
|
4
|
+
page?: string | number;
|
|
5
|
+
status?: string;
|
|
6
|
+
sort?: string;
|
|
7
|
+
dir?: string;
|
|
8
|
+
owner?: string;
|
|
9
|
+
removeOwner?: boolean;
|
|
10
|
+
startDate?: string;
|
|
11
|
+
removeStartDate?: boolean;
|
|
12
|
+
targetDate?: string;
|
|
13
|
+
removeTargetDate?: boolean;
|
|
14
|
+
label?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function runInitiative(ctx: Context, action: string, initiativeId: string | undefined, options: InitiativeOptions): Promise<Result>;
|