@appforge-ci/admin-cli 0.1.1

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/commands/agents.d.ts +55 -0
  4. package/dist/commands/agents.d.ts.map +1 -0
  5. package/dist/commands/agents.js +436 -0
  6. package/dist/commands/agents.js.map +1 -0
  7. package/dist/commands/agents.test.d.ts +2 -0
  8. package/dist/commands/agents.test.d.ts.map +1 -0
  9. package/dist/commands/agents.test.js +127 -0
  10. package/dist/commands/agents.test.js.map +1 -0
  11. package/dist/commands/login.d.ts +38 -0
  12. package/dist/commands/login.d.ts.map +1 -0
  13. package/dist/commands/login.js +128 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/login.test.d.ts +2 -0
  16. package/dist/commands/login.test.d.ts.map +1 -0
  17. package/dist/commands/login.test.js +69 -0
  18. package/dist/commands/login.test.js.map +1 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +34 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/bootstrap.d.ts +21 -0
  24. package/dist/lib/bootstrap.d.ts.map +1 -0
  25. package/dist/lib/bootstrap.js +242 -0
  26. package/dist/lib/bootstrap.js.map +1 -0
  27. package/dist/lib/bootstrap.test.d.ts +2 -0
  28. package/dist/lib/bootstrap.test.d.ts.map +1 -0
  29. package/dist/lib/bootstrap.test.js +59 -0
  30. package/dist/lib/bootstrap.test.js.map +1 -0
  31. package/dist/lib/config.d.ts +27 -0
  32. package/dist/lib/config.d.ts.map +1 -0
  33. package/dist/lib/config.js +61 -0
  34. package/dist/lib/config.js.map +1 -0
  35. package/dist/lib/config.test.d.ts +2 -0
  36. package/dist/lib/config.test.d.ts.map +1 -0
  37. package/dist/lib/config.test.js +120 -0
  38. package/dist/lib/config.test.js.map +1 -0
  39. package/dist/lib/prompt.d.ts +11 -0
  40. package/dist/lib/prompt.d.ts.map +1 -0
  41. package/dist/lib/prompt.js +54 -0
  42. package/dist/lib/prompt.js.map +1 -0
  43. package/dist/lib/prompt.test.d.ts +2 -0
  44. package/dist/lib/prompt.test.d.ts.map +1 -0
  45. package/dist/lib/prompt.test.js +22 -0
  46. package/dist/lib/prompt.test.js.map +1 -0
  47. package/dist/lib/two-factor.d.ts +21 -0
  48. package/dist/lib/two-factor.d.ts.map +1 -0
  49. package/dist/lib/two-factor.js +31 -0
  50. package/dist/lib/two-factor.js.map +1 -0
  51. package/dist/lib/two-factor.test.d.ts +2 -0
  52. package/dist/lib/two-factor.test.d.ts.map +1 -0
  53. package/dist/lib/two-factor.test.js +51 -0
  54. package/dist/lib/two-factor.test.js.map +1 -0
  55. package/dist/test-support/home-sandbox.d.ts +3 -0
  56. package/dist/test-support/home-sandbox.d.ts.map +1 -0
  57. package/dist/test-support/home-sandbox.js +47 -0
  58. package/dist/test-support/home-sandbox.js.map +1 -0
  59. package/package.json +33 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/lib/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,QAAgB;IAC3C,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC;AAED,MAAM,MAAM,GAAG,MAAM,CAAC;AACtB,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC5B,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,GAAG,EAAE;YAClB,KAAK,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC;YAC1B,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QACF,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC5C,MAAM,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC5C,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;gBACxB,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;gBACtC,MAAM,EAAE,CAAC;gBACT,OAAO;YACT,CAAC;YACD,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9B,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3B,OAAO;YACT,CAAC;YACD,KAAK,IAAI,KAAK,CAAC;QACjB,CAAC,CAAC;QACF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ import "../test-support/home-sandbox.js";
2
+ //# sourceMappingURL=prompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.test.d.ts","sourceRoot":"","sources":["../../src/lib/prompt.test.ts"],"names":[],"mappings":"AACA,OAAO,iCAAiC,CAAC"}
@@ -0,0 +1,22 @@
1
+ // Must stay the FIRST import — see the comment below the imports.
2
+ import "../test-support/home-sandbox.js";
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { Readable } from "node:stream";
6
+ import { promptHidden } from "./prompt.js";
7
+ test("promptHidden resolves when the whole line arrives as one chunk (piped stdin)", async () => {
8
+ const stdin = new Readable({ read() { } });
9
+ stdin.isTTY = false;
10
+ const originalStdin = process.stdin;
11
+ Object.defineProperty(process, "stdin", { value: stdin, configurable: true });
12
+ try {
13
+ const resultPromise = promptHidden("Password: ");
14
+ stdin.push("correct-horse-battery-staple\n");
15
+ const result = await resultPromise;
16
+ assert.equal(result, "correct-horse-battery-staple");
17
+ }
18
+ finally {
19
+ Object.defineProperty(process, "stdin", { value: originalStdin, configurable: true });
20
+ }
21
+ });
22
+ //# sourceMappingURL=prompt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.test.js","sourceRoot":"","sources":["../../src/lib/prompt.test.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,OAAO,iCAAiC,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,IAAI,CAAC,8EAA8E,EAAE,KAAK,IAAI,EAAE;IAC9F,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,EAAE,IAAI,KAAI,CAAC,EAAE,CAA4C,CAAC;IACrF,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC;IACpC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9E,IAAI,CAAC;QACH,MAAM,aAAa,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;IACvD,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface TwoFactorCliOptions {
2
+ totpCode?: string;
3
+ recoveryCode?: string;
4
+ }
5
+ /**
6
+ * Wraps a `login(twoFactor?)` call to handle the 2FA challenge
7
+ * (`docs/security.md`'s "2FA (TOTP)" note): a plain login throws
8
+ * `ApiError(401, { requiresTotp: true })` once an account has 2FA
9
+ * enabled — see `apps/api/src/routes/auth.ts`. Before this, an account
10
+ * with 2FA enabled could only log in from the dashboard; the password
11
+ * path here just threw that unhandled error (`--web` already had its own
12
+ * separate device-authorization flow, unaffected).
13
+ *
14
+ * `precomputed` lets a non-interactive caller (CI, scripting) pass
15
+ * `--totp-code`/`--recovery-code` up front and skip the round trip.
16
+ * Otherwise `promptForCode` is called once, after the password is
17
+ * already known-good — same two-step shape as the dashboard's login form
18
+ * (`apps/dashboard/src/pages/Login.tsx`).
19
+ */
20
+ export declare function loginWithTwoFactor<T>(login: (twoFactor?: TwoFactorCliOptions) => Promise<T>, precomputed: TwoFactorCliOptions, promptForCode: () => Promise<TwoFactorCliOptions>): Promise<T>;
21
+ //# sourceMappingURL=two-factor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-factor.d.ts","sourceRoot":"","sources":["../../src/lib/two-factor.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,CAAC,CAAC,EACtD,WAAW,EAAE,mBAAmB,EAChC,aAAa,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAYZ"}
@@ -0,0 +1,31 @@
1
+ import { ApiError } from "@appforge-ci/core";
2
+ /**
3
+ * Wraps a `login(twoFactor?)` call to handle the 2FA challenge
4
+ * (`docs/security.md`'s "2FA (TOTP)" note): a plain login throws
5
+ * `ApiError(401, { requiresTotp: true })` once an account has 2FA
6
+ * enabled — see `apps/api/src/routes/auth.ts`. Before this, an account
7
+ * with 2FA enabled could only log in from the dashboard; the password
8
+ * path here just threw that unhandled error (`--web` already had its own
9
+ * separate device-authorization flow, unaffected).
10
+ *
11
+ * `precomputed` lets a non-interactive caller (CI, scripting) pass
12
+ * `--totp-code`/`--recovery-code` up front and skip the round trip.
13
+ * Otherwise `promptForCode` is called once, after the password is
14
+ * already known-good — same two-step shape as the dashboard's login form
15
+ * (`apps/dashboard/src/pages/Login.tsx`).
16
+ */
17
+ export async function loginWithTwoFactor(login, precomputed, promptForCode) {
18
+ if (precomputed.totpCode || precomputed.recoveryCode) {
19
+ return login(precomputed);
20
+ }
21
+ try {
22
+ return await login();
23
+ }
24
+ catch (err) {
25
+ if (!(err instanceof ApiError) || !err.body?.requiresTotp) {
26
+ throw err;
27
+ }
28
+ }
29
+ return login(await promptForCode());
30
+ }
31
+ //# sourceMappingURL=two-factor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-factor.js","sourceRoot":"","sources":["../../src/lib/two-factor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAO7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAsD,EACtD,WAAgC,EAChC,aAAiD;IAEjD,IAAI,WAAW,CAAC,QAAQ,IAAI,WAAW,CAAC,YAAY,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC,WAAW,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,EAAE,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,YAAY,QAAQ,CAAC,IAAI,CAAE,GAAG,CAAC,IAA0C,EAAE,YAAY,EAAE,CAAC;YACjG,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,aAAa,EAAE,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import "../test-support/home-sandbox.js";
2
+ //# sourceMappingURL=two-factor.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-factor.test.d.ts","sourceRoot":"","sources":["../../src/lib/two-factor.test.ts"],"names":[],"mappings":"AACA,OAAO,iCAAiC,CAAC"}
@@ -0,0 +1,51 @@
1
+ // Must stay the FIRST import — see the comment below the imports.
2
+ import "../test-support/home-sandbox.js";
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { ApiError } from "@appforge-ci/core";
6
+ import { loginWithTwoFactor } from "./two-factor.js";
7
+ test("logs in directly when the account has no 2FA enabled", async () => {
8
+ let calls = 0;
9
+ const result = await loginWithTwoFactor(async (twoFactor) => {
10
+ calls++;
11
+ assert.equal(twoFactor, undefined);
12
+ return { token: "t" };
13
+ }, {}, async () => {
14
+ throw new Error("should not prompt");
15
+ });
16
+ assert.deepEqual(result, { token: "t" });
17
+ assert.equal(calls, 1);
18
+ });
19
+ test("uses --totp-code up front without ever calling the plain login first", async () => {
20
+ const calls = [];
21
+ const result = await loginWithTwoFactor(async (twoFactor) => {
22
+ calls.push(twoFactor);
23
+ return { token: "t" };
24
+ }, { totpCode: "123456" }, async () => {
25
+ throw new Error("should not prompt");
26
+ });
27
+ assert.deepEqual(result, { token: "t" });
28
+ assert.deepEqual(calls, [{ totpCode: "123456" }]);
29
+ });
30
+ test("prompts for a code and retries once the server asks for 2FA", async () => {
31
+ let attempt = 0;
32
+ const result = await loginWithTwoFactor(async (twoFactor) => {
33
+ attempt++;
34
+ if (attempt === 1) {
35
+ assert.equal(twoFactor, undefined);
36
+ throw new ApiError(401, { error: "totp code required", requiresTotp: true });
37
+ }
38
+ assert.deepEqual(twoFactor, { recoveryCode: "aaaa-bbbb-cccc-dddd" });
39
+ return { token: "t" };
40
+ }, {}, async () => ({ recoveryCode: "aaaa-bbbb-cccc-dddd" }));
41
+ assert.deepEqual(result, { token: "t" });
42
+ assert.equal(attempt, 2);
43
+ });
44
+ test("propagates a non-2FA login error without prompting", async () => {
45
+ await assert.rejects(loginWithTwoFactor(async () => {
46
+ throw new ApiError(401, { error: "invalid credentials" });
47
+ }, {}, async () => {
48
+ throw new Error("should not prompt");
49
+ }), /invalid credentials/);
50
+ });
51
+ //# sourceMappingURL=two-factor.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-factor.test.js","sourceRoot":"","sources":["../../src/lib/two-factor.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** The throwaway directory os.homedir() resolves to for the rest of this test process. */
2
+ export declare const SANDBOX_HOME: string;
3
+ //# sourceMappingURL=home-sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-sandbox.d.ts","sourceRoot":"","sources":["../../src/test-support/home-sandbox.ts"],"names":[],"mappings":"AAgDA,0FAA0F;AAC1F,eAAO,MAAM,YAAY,EAAE,MAAoB,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Import this FIRST — before the module under test — from any test file
3
+ * that reads, writes or deletes a path derived from os.homedir().
4
+ *
5
+ * `pnpm test` already sandboxes $HOME for every test process via
6
+ * scripts/test-home-sandbox.mjs (the canonical copy of this logic,
7
+ * wired in with `tsx --import`). This module exists for the other way
8
+ * tests get run: a single file invoked directly with `npx tsx --test
9
+ * src/lib/config.test.ts` and no preload. That exact invocation, against
10
+ * this package's tests, deleted the live build agent's
11
+ * ~/.appforge-agent/config.json on the maintainer's Mac on 2026-09-09 and
12
+ * 2026-09-11 (and the admin CLI's ~/.appforge-admin/config.json) — the
13
+ * daemon only reads that file at startup, so the damage surfaced days
14
+ * later as a crash loop. See docs/done/2026-09-13-tests-never-touch-real-home.md.
15
+ *
16
+ * ESM evaluates imports in source order, so as long as this is the first
17
+ * import in the test file, $HOME is redirected before the module under
18
+ * test computes its `join(homedir(), ...)` constants. If the sandbox is
19
+ * somehow not in effect, this throws rather than let the file run.
20
+ */
21
+ import { mkdtempSync, rmSync } from "node:fs";
22
+ import { homedir, tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ if (!process.env.APPFORGE_TEST_HOME_SANDBOX) {
25
+ const realHome = process.env.HOME ?? homedir();
26
+ const sandbox = mkdtempSync(join(tmpdir(), "appforge-test-home-"));
27
+ process.env.APPFORGE_TEST_REAL_HOME = realHome;
28
+ process.env.HOME = sandbox;
29
+ process.env.USERPROFILE = sandbox;
30
+ process.env.APPFORGE_TEST_HOME_SANDBOX = sandbox;
31
+ process.on("exit", () => {
32
+ try {
33
+ rmSync(sandbox, { recursive: true, force: true });
34
+ }
35
+ catch {
36
+ /* best effort */
37
+ }
38
+ });
39
+ }
40
+ const sandboxHome = process.env.APPFORGE_TEST_HOME_SANDBOX;
41
+ if (!sandboxHome || homedir() !== sandboxHome || sandboxHome === process.env.APPFORGE_TEST_REAL_HOME) {
42
+ throw new Error(`test HOME sandbox is not in effect (homedir()=${homedir()}, sandbox=${sandboxHome}) — ` +
43
+ `refusing to run tests that could touch a real home directory`);
44
+ }
45
+ /** The throwaway directory os.homedir() resolves to for the rest of this test process. */
46
+ export const SANDBOX_HOME = sandboxHome;
47
+ //# sourceMappingURL=home-sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"home-sandbox.js","sourceRoot":"","sources":["../../src/test-support/home-sandbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;IAC/C,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,QAAQ,CAAC;IAC/C,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;IAC3B,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,OAAO,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,0BAA0B,GAAG,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,IAAI,CAAC;YACH,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC;AAC3D,IAAI,CAAC,WAAW,IAAI,OAAO,EAAE,KAAK,WAAW,IAAI,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC;IACrG,MAAM,IAAI,KAAK,CACb,iDAAiD,OAAO,EAAE,aAAa,WAAW,MAAM;QACtF,8DAA8D,CACjE,CAAC;AACJ,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,YAAY,GAAW,WAAW,CAAC"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@appforge-ci/admin-cli",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "type": "module",
12
+ "bin": {
13
+ "appforge-admin": "./dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "chalk": "^5.3.0",
17
+ "commander": "^12.1.0",
18
+ "@appforge-ci/agent": "0.1.1",
19
+ "@appforge-ci/core": "0.1.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.16.5",
23
+ "tsx": "^4.23.13",
24
+ "typescript": "^5.6.3"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "dev": "tsx src/index.ts",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit",
30
+ "lint": "echo 'no lint configured for admin-cli'",
31
+ "test": "tsx --import ../../scripts/test-home-sandbox.mjs --test $(find src -name '*.test.ts') --concurrency=4"
32
+ }
33
+ }