@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/commands/agents.d.ts +55 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +436 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/agents.test.d.ts +2 -0
- package/dist/commands/agents.test.d.ts.map +1 -0
- package/dist/commands/agents.test.js +127 -0
- package/dist/commands/agents.test.js.map +1 -0
- package/dist/commands/login.d.ts +38 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +128 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/login.test.d.ts +2 -0
- package/dist/commands/login.test.d.ts.map +1 -0
- package/dist/commands/login.test.js +69 -0
- package/dist/commands/login.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/bootstrap.d.ts +21 -0
- package/dist/lib/bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap.js +242 -0
- package/dist/lib/bootstrap.js.map +1 -0
- package/dist/lib/bootstrap.test.d.ts +2 -0
- package/dist/lib/bootstrap.test.d.ts.map +1 -0
- package/dist/lib/bootstrap.test.js +59 -0
- package/dist/lib/bootstrap.test.js.map +1 -0
- package/dist/lib/config.d.ts +27 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +61 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/config.test.d.ts +2 -0
- package/dist/lib/config.test.d.ts.map +1 -0
- package/dist/lib/config.test.js +120 -0
- package/dist/lib/config.test.js.map +1 -0
- package/dist/lib/prompt.d.ts +11 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +54 -0
- package/dist/lib/prompt.js.map +1 -0
- package/dist/lib/prompt.test.d.ts +2 -0
- package/dist/lib/prompt.test.d.ts.map +1 -0
- package/dist/lib/prompt.test.js +22 -0
- package/dist/lib/prompt.test.js.map +1 -0
- package/dist/lib/two-factor.d.ts +21 -0
- package/dist/lib/two-factor.d.ts.map +1 -0
- package/dist/lib/two-factor.js +31 -0
- package/dist/lib/two-factor.js.map +1 -0
- package/dist/lib/two-factor.test.d.ts +2 -0
- package/dist/lib/two-factor.test.d.ts.map +1 -0
- package/dist/lib/two-factor.test.js +51 -0
- package/dist/lib/two-factor.test.js.map +1 -0
- package/dist/test-support/home-sandbox.d.ts +3 -0
- package/dist/test-support/home-sandbox.d.ts.map +1 -0
- package/dist/test-support/home-sandbox.js +47 -0
- package/dist/test-support/home-sandbox.js.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|