@wujiland/cli 0.0.0-stage → 1.0.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/README.md +12 -2
- package/dist/app.d.ts +16 -0
- package/dist/app.js +59 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +81 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +4 -0
- package/dist/commands/access.d.ts +14 -0
- package/dist/commands/access.js +201 -0
- package/dist/commands/ci.d.ts +27 -0
- package/dist/commands/ci.js +289 -0
- package/dist/commands/create.d.ts +8 -0
- package/dist/commands/create.js +154 -0
- package/dist/commands/local.d.ts +5 -0
- package/dist/commands/local.js +185 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +59 -0
- package/dist/commands/ops.d.ts +21 -0
- package/dist/commands/ops.js +178 -0
- package/dist/commands/ship.d.ts +7 -0
- package/dist/commands/ship.js +182 -0
- package/dist/context.d.ts +83 -0
- package/dist/context.js +63 -0
- package/dist/github.d.ts +16 -0
- package/dist/github.js +63 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +123 -0
- package/dist/manager.d.ts +10 -0
- package/dist/manager.js +51 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +56 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.js +83 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +35 -4
package/dist/github.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Man Hay's own `gh` and `git` (INCA-27). Each gh call gets a token for the named account in its own
|
|
2
|
+
// environment only; the active gh account is never switched and the token is never kept (06-cli.md §4).
|
|
3
|
+
import { INSTALL, checkTool } from "./auth.js";
|
|
4
|
+
import { CliError, EXIT } from "./context.js";
|
|
5
|
+
export const appRepo = (slug) => `inca-app-${slug}`;
|
|
6
|
+
export async function ghFor(deps, user) {
|
|
7
|
+
await checkTool(deps, "gh", ["--version"], INSTALL.gh);
|
|
8
|
+
let token = null;
|
|
9
|
+
const getToken = async () => {
|
|
10
|
+
if (token !== null)
|
|
11
|
+
return token;
|
|
12
|
+
const r = await deps.run("gh", ["auth", "token", "--user", user]);
|
|
13
|
+
const t = r.stdout.trim();
|
|
14
|
+
if (r.code !== 0 || t === "") {
|
|
15
|
+
throw new CliError(EXIT.AUTH, "GH_NOT_LOGGED_IN", `gh has no login for ${user}.`, `gh auth login (as ${user}), then gh auth switch back to your usual account`);
|
|
16
|
+
}
|
|
17
|
+
token = t;
|
|
18
|
+
return t;
|
|
19
|
+
};
|
|
20
|
+
const run = async (args, opts = {}) => {
|
|
21
|
+
const env = { ...deps.env, GH_TOKEN: await getToken(), GH_PROMPT_DISABLED: "1" };
|
|
22
|
+
return deps.run("gh", args, { ...opts, env });
|
|
23
|
+
};
|
|
24
|
+
const ok = async (args, opts = {}) => {
|
|
25
|
+
const r = await run(args, opts);
|
|
26
|
+
if (r.code !== 0)
|
|
27
|
+
throw new CliError(EXIT.OUTSIDE, "GH_FAILED", `gh ${args.slice(0, 2).join(" ")} exited ${r.code}: ${firstLine(r.stderr)}`);
|
|
28
|
+
return r;
|
|
29
|
+
};
|
|
30
|
+
return { owner: user, run, ok };
|
|
31
|
+
}
|
|
32
|
+
export const firstLine = (s) => s.trim().split("\n")[0] ?? "";
|
|
33
|
+
export async function git(deps, cwd, args, opts = {}) {
|
|
34
|
+
try {
|
|
35
|
+
return await deps.run("git", args, { ...opts, cwd });
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof CliError && e.code === "TOOL_MISSING")
|
|
39
|
+
throw new CliError(EXIT.OUTSIDE, "TOOL_MISSING", "git is not installed.", INSTALL.git);
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function gitOk(deps, cwd, args, opts = {}) {
|
|
44
|
+
const r = await git(deps, cwd, args, opts);
|
|
45
|
+
if (r.code !== 0)
|
|
46
|
+
throw new CliError(EXIT.OUTSIDE, "GIT_FAILED", `git ${args[0]} exited ${r.code}: ${firstLine(r.stderr)}`);
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
/** Declared names present as `<PREFIX>_<NAME>` repository secrets. */
|
|
50
|
+
export async function secretNames(gh, repo) {
|
|
51
|
+
const r = await gh.ok(["secret", "list", "-R", `${gh.owner}/${repo}`, "--json", "name"]);
|
|
52
|
+
let list;
|
|
53
|
+
try {
|
|
54
|
+
list = JSON.parse(r.stdout);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new CliError(EXIT.OUTSIDE, "GH_FAILED", "gh secret list did not return JSON.");
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(list))
|
|
60
|
+
throw new CliError(EXIT.OUTSIDE, "GH_FAILED", "gh secret list did not return a list.");
|
|
61
|
+
return new Set(list.map((x) => x.name).filter((n) => typeof n === "string"));
|
|
62
|
+
}
|
|
63
|
+
export const prefixFor = (env) => (env === "uat" ? "UAT_" : "PROD_");
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { main, COMMANDS } from "./main.js";
|
|
2
|
+
export { nodeDeps, runProcess } from "./node.js";
|
|
3
|
+
export { EXIT, CliError } from "./context.js";
|
|
4
|
+
export type { Deps, Outcome, Pending, RunOptions, RunResult, Runner } from "./context.js";
|
|
5
|
+
export { CLI_VERSION } from "./version.js";
|
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParseArgsConfig } from "node:util";
|
|
2
|
+
import type { Ctx, Deps, Outcome } from "./context.js";
|
|
3
|
+
type Options = NonNullable<ParseArgsConfig["options"]>;
|
|
4
|
+
export type Command = {
|
|
5
|
+
options: Options;
|
|
6
|
+
/** [min, max] positional arguments. */
|
|
7
|
+
args: [number, number];
|
|
8
|
+
usage: string;
|
|
9
|
+
run: (ctx: Ctx) => Promise<Outcome>;
|
|
10
|
+
};
|
|
11
|
+
export declare const COMMANDS: Record<string, Command>;
|
|
12
|
+
export declare function main(argv: string[], deps: Deps): Promise<number>;
|
|
13
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Argument parsing, dispatch, and the output contract: --json prints exactly one JSON document on
|
|
2
|
+
// stdout; progress and warnings go to stderr; exit codes per 06-cli.md §7.
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { CliError, EXIT, usage } from "./context.js";
|
|
5
|
+
import { accessRequest, decommission, purge } from "./commands/access.js";
|
|
6
|
+
import { ciDeploy } from "./commands/ci.js";
|
|
7
|
+
import { create } from "./commands/create.js";
|
|
8
|
+
import { configCheck, configRender, dev, envAdd } from "./commands/local.js";
|
|
9
|
+
import { login, whoami } from "./commands/login.js";
|
|
10
|
+
import { accessShow, logs, ls, rollback, status, versions } from "./commands/ops.js";
|
|
11
|
+
import { deploy, envLs, envPush, promote } from "./commands/ship.js";
|
|
12
|
+
import { CLI_VERSION } from "./version.js";
|
|
13
|
+
const s = { type: "string" };
|
|
14
|
+
const b = { type: "boolean" };
|
|
15
|
+
const many = { type: "string", multiple: true };
|
|
16
|
+
// Only `login` has --manager (INCA-33): no other command can change where the CLI sends the token.
|
|
17
|
+
export const COMMANDS = {
|
|
18
|
+
login: { options: { manager: s, "github-user": s, app: s, broker: s, infra: s }, args: [0, 0], usage: "inca login --manager https://manager.<domain> [--github-user <account>] [--app <slug>]", run: login },
|
|
19
|
+
whoami: { options: {}, args: [0, 0], usage: "inca whoami", run: whoami },
|
|
20
|
+
create: { options: { description: s, storage: s }, args: [1, 1], usage: 'inca create <slug> --description "…" [--storage d1:DB,r2:MEDIA]', run: create },
|
|
21
|
+
dev: { options: {}, args: [0, 0], usage: "inca dev [-- <wrangler args>]", run: dev },
|
|
22
|
+
"config check": { options: { drift: b, contract: b, broker: s }, args: [0, 0], usage: "inca config check [--drift] [--contract [--broker <url>]]", run: configCheck },
|
|
23
|
+
"config render": { options: { mode: s, env: s, out: s, domain: s }, args: [0, 0], usage: "inca config render [--mode local|build] [--env uat|prod] [--out <dir>] [--domain <domain>]", run: configRender },
|
|
24
|
+
deploy: { options: { env: s, wait: b }, args: [0, 0], usage: "inca deploy --env uat|prod [--wait]", run: deploy },
|
|
25
|
+
promote: { options: { wait: b }, args: [0, 0], usage: "inca promote [--wait]", run: promote },
|
|
26
|
+
status: { options: {}, args: [0, 1], usage: "inca status [<slug>]", run: status },
|
|
27
|
+
ls: { options: {}, args: [0, 0], usage: "inca ls", run: ls },
|
|
28
|
+
logs: { options: { env: s, since: s, until: s, level: s, request: s, follow: b, interval: s }, args: [1, 1], usage: "inca logs <slug> [--env] [--since] [--until] [--level] [--request] [--follow [--interval <s>]]", run: logs },
|
|
29
|
+
"env add": { options: {}, args: [1, 1], usage: "inca env add <NAME>", run: envAdd },
|
|
30
|
+
"env push": { options: { env: s, from: s }, args: [0, 0], usage: "inca env push --env uat|prod [--from <file>]", run: envPush },
|
|
31
|
+
"env ls": { options: { env: s }, args: [0, 0], usage: "inca env ls [--env uat|prod]", run: envLs },
|
|
32
|
+
"access show": { options: {}, args: [0, 1], usage: "inca access show [<slug>]", run: accessShow },
|
|
33
|
+
"access request": { options: { named: many, public: b, private: b, bypass: many, infra: s }, args: [1, 1], usage: "inca access request <slug> [--named <email>…] [--public] [--private] [--bypass <path>…] [--infra <dir>]", run: accessRequest },
|
|
34
|
+
versions: { options: { env: s }, args: [0, 1], usage: "inca versions [<slug>] [--env]", run: versions },
|
|
35
|
+
rollback: { options: { env: s, to: s }, args: [0, 1], usage: "inca rollback [<slug>] --env uat|prod [--to <version>]", run: rollback },
|
|
36
|
+
decommission: { options: { infra: s }, args: [1, 1], usage: "inca decommission <slug> [--infra <dir>]", run: decommission },
|
|
37
|
+
purge: { options: {}, args: [1, 1], usage: "inca purge <slug>", run: purge },
|
|
38
|
+
"ci deploy": { options: { artifact: s, env: s, broker: s }, args: [0, 0], usage: "inca ci deploy --artifact <dir> --env uat|prod --broker <url>", run: ciDeploy },
|
|
39
|
+
};
|
|
40
|
+
const GLOBAL = { json: b, quiet: b, yes: b, help: b };
|
|
41
|
+
const GROUPS = new Set(["config", "env", "access", "ci"]);
|
|
42
|
+
function helpText() {
|
|
43
|
+
return ["inca — the command line for the whole life of an Inca app.", "", ...Object.values(COMMANDS).map((c) => ` ${c.usage}`), "", "Global: --json --quiet --yes --help --version"].join("\n");
|
|
44
|
+
}
|
|
45
|
+
export async function main(argv, deps) {
|
|
46
|
+
const dash = argv.indexOf("--");
|
|
47
|
+
const head = dash === -1 ? argv : argv.slice(0, dash);
|
|
48
|
+
const passthrough = dash === -1 ? [] : argv.slice(dash + 1);
|
|
49
|
+
const json = head.includes("--json");
|
|
50
|
+
const quiet = head.includes("--quiet");
|
|
51
|
+
const errOut = (e) => {
|
|
52
|
+
if (json)
|
|
53
|
+
deps.stdout(`${JSON.stringify({ ok: false, code: e.code, message: e.message, next: e.next, ...e.detail })}\n`);
|
|
54
|
+
else
|
|
55
|
+
deps.stderr(`error ${e.code}: ${e.message}${e.next ? `\n next: ${e.next}` : ""}\n`);
|
|
56
|
+
return e.exit;
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
if (head[0] === "--version" || head[0] === "-v") {
|
|
60
|
+
deps.stdout(json ? `${JSON.stringify({ ok: true, version: CLI_VERSION })}\n` : `${CLI_VERSION}\n`);
|
|
61
|
+
return EXIT.OK;
|
|
62
|
+
}
|
|
63
|
+
const words = head.filter((a) => !a.startsWith("-"));
|
|
64
|
+
if (head.length === 0 || (words.length === 0 && head.includes("--help"))) {
|
|
65
|
+
deps.stdout(json ? `${JSON.stringify({ ok: true, usage: Object.values(COMMANDS).map((c) => c.usage) })}\n` : `${helpText()}\n`);
|
|
66
|
+
return head.length === 0 ? EXIT.USAGE : EXIT.OK;
|
|
67
|
+
}
|
|
68
|
+
const first = head[0];
|
|
69
|
+
const name = GROUPS.has(first) && head[1] !== undefined && !head[1].startsWith("-") ? `${first} ${head[1]}` : first;
|
|
70
|
+
const command = COMMANDS[name];
|
|
71
|
+
if (!command)
|
|
72
|
+
throw usage(`Unknown command ${JSON.stringify(name)}.`, "inca --help");
|
|
73
|
+
if (passthrough.length && name !== "dev")
|
|
74
|
+
throw usage("Only inca dev passes arguments after --.");
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = parseArgs({ args: head.slice(name.split(" ").length), options: { ...GLOBAL, ...command.options }, allowPositionals: true, strict: true });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
throw usage(`${e.message}`, command.usage);
|
|
81
|
+
}
|
|
82
|
+
const { values, positionals } = parsed;
|
|
83
|
+
if (values.help) {
|
|
84
|
+
deps.stdout(json ? `${JSON.stringify({ ok: true, usage: command.usage })}\n` : `${command.usage}\n`);
|
|
85
|
+
return EXIT.OK;
|
|
86
|
+
}
|
|
87
|
+
const [min, max] = command.args;
|
|
88
|
+
if (positionals.length < min || positionals.length > max)
|
|
89
|
+
throw usage(`Wrong number of arguments.`, command.usage);
|
|
90
|
+
const ctx = {
|
|
91
|
+
deps,
|
|
92
|
+
flags: values,
|
|
93
|
+
positionals,
|
|
94
|
+
passthrough,
|
|
95
|
+
json,
|
|
96
|
+
yes: values.yes === true,
|
|
97
|
+
warn: (line) => deps.stderr(`warning: ${line}\n`),
|
|
98
|
+
progress: (line) => {
|
|
99
|
+
if (!quiet)
|
|
100
|
+
deps.stderr(`${line}\n`);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const outcome = await command.run(ctx);
|
|
104
|
+
const pending = outcome.pending ?? [];
|
|
105
|
+
const exit = outcome.exit ?? (pending.length ? EXIT.PENDING : EXIT.OK);
|
|
106
|
+
if (json) {
|
|
107
|
+
deps.stdout(`${JSON.stringify({ ok: true, ...outcome.data, pending })}\n`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const lines = quiet ? [] : [...outcome.human];
|
|
111
|
+
for (const p of pending)
|
|
112
|
+
lines.push(`PENDING ${p.kind}: ${p.message}${p.next ? `\n next: ${p.next}` : ""}`);
|
|
113
|
+
if (lines.length)
|
|
114
|
+
deps.stdout(`${lines.join("\n")}\n`);
|
|
115
|
+
}
|
|
116
|
+
return exit;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
if (e instanceof CliError)
|
|
120
|
+
return errOut(e);
|
|
121
|
+
return errOut(new CliError(EXIT.UNEXPECTED, "UNEXPECTED", `Unexpected failure: ${e.message ?? String(e)}`, "This is a bug in inca; report it with the command you ran."));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Deps } from "./context.js";
|
|
2
|
+
import type { CliConfig } from "./store.js";
|
|
3
|
+
export type ManagerClient = {
|
|
4
|
+
url: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
config: CliConfig;
|
|
7
|
+
call: <T = Record<string, unknown>>(method: "GET" | "POST", path: string, body?: unknown) => Promise<T>;
|
|
8
|
+
};
|
|
9
|
+
export declare const managerAccount: (managerUrl: string) => string;
|
|
10
|
+
export declare function managerClient(deps: Deps): Promise<ManagerClient>;
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The manager's HTTP API (04-manager.md §6) with the owner's Access token in `cf-access-token`.
|
|
2
|
+
import { loadToken } from "./auth.js";
|
|
3
|
+
import { CliError, EXIT } from "./context.js";
|
|
4
|
+
import { parseManagerUrl, requireCliConfig } from "./store.js";
|
|
5
|
+
function exitFor(status, code) {
|
|
6
|
+
if (status === 401 || code === "NOT_OWNER")
|
|
7
|
+
return EXIT.AUTH;
|
|
8
|
+
if (status === 404)
|
|
9
|
+
return EXIT.NOT_FOUND;
|
|
10
|
+
if (status >= 500)
|
|
11
|
+
return EXIT.OUTSIDE;
|
|
12
|
+
return EXIT.REFUSED;
|
|
13
|
+
}
|
|
14
|
+
export const managerAccount = (managerUrl) => `access:${new URL(managerUrl).hostname}`;
|
|
15
|
+
export async function managerClient(deps) {
|
|
16
|
+
const config = await requireCliConfig(deps);
|
|
17
|
+
const { url, domain } = parseManagerUrl(config.manager_url);
|
|
18
|
+
const token = await loadToken(deps, managerAccount(url));
|
|
19
|
+
const call = async (method, path, body) => {
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await deps.fetch(`${url}${path}`, {
|
|
23
|
+
method,
|
|
24
|
+
redirect: "manual",
|
|
25
|
+
headers: { "cf-access-token": token, accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
26
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw new CliError(EXIT.OUTSIDE, "MANAGER_UNREACHABLE", `The manager did not answer: ${e.message}.`);
|
|
31
|
+
}
|
|
32
|
+
// Access answers an expired session with a redirect to its login page, not with JSON.
|
|
33
|
+
if (res.status >= 300 && res.status < 400)
|
|
34
|
+
throw new CliError(EXIT.AUTH, "ACCESS_SESSION", "Access wants a new sign-in.", "inca login");
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = await res.json();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new CliError(res.status === 401 || res.status === 403 ? EXIT.AUTH : EXIT.OUTSIDE, "MANAGER_NOT_JSON", `The manager answered ${res.status} without JSON.`, res.status === 401 || res.status === 403 ? "inca login" : null);
|
|
41
|
+
}
|
|
42
|
+
const r = parsed;
|
|
43
|
+
if (r.ok !== true) {
|
|
44
|
+
const code = typeof r.code === "string" ? r.code : `HTTP_${res.status}`;
|
|
45
|
+
const { ok: _ok, code: _c, message: _m, next: _n, ...rest } = r;
|
|
46
|
+
throw new CliError(exitFor(res.status, code), code, typeof r.message === "string" ? r.message : `The manager refused (${res.status}).`, typeof r.next === "string" ? r.next : null, rest);
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
};
|
|
50
|
+
return { url, domain, config, call };
|
|
51
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Deps, RunOptions, RunResult } from "./context.js";
|
|
2
|
+
export declare const childEnv: (env: Record<string, string | undefined>) => {
|
|
3
|
+
[k: string]: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function runProcess(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
|
|
6
|
+
export declare function nodeDeps(): Deps;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The real outside world for bin.ts: child processes by argument array (never a shell), the terminal, fetch.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { CliError, EXIT } from "./context.js";
|
|
6
|
+
// The Access token and CI's secret values are for this process only; no child (gh, git, wrangler) gets them.
|
|
7
|
+
const PRIVATE_ENV = new Set(["INCA_TOKEN", "INCA_SECRETS_JSON"]);
|
|
8
|
+
export const childEnv = (env) => Object.fromEntries(Object.entries(env).filter((e) => e[1] !== undefined && !PRIVATE_ENV.has(e[0])));
|
|
9
|
+
export function runProcess(cmd, args, opts = {}) {
|
|
10
|
+
return new Promise((resolvePromise, reject) => {
|
|
11
|
+
const child = spawn(cmd, args, {
|
|
12
|
+
cwd: opts.cwd,
|
|
13
|
+
env: childEnv(opts.env ?? process.env),
|
|
14
|
+
shell: false,
|
|
15
|
+
stdio: [opts.input !== undefined ? "pipe" : opts.inherit ? "inherit" : "ignore", opts.inherit ? "inherit" : "pipe", opts.inherit ? "inherit" : "pipe"],
|
|
16
|
+
});
|
|
17
|
+
let stdout = "";
|
|
18
|
+
let stderr = "";
|
|
19
|
+
child.stdout?.on("data", (d) => (stdout += d.toString("utf8")));
|
|
20
|
+
child.stderr?.on("data", (d) => (stderr += d.toString("utf8")));
|
|
21
|
+
child.on("error", (e) => {
|
|
22
|
+
if (e.code === "ENOENT")
|
|
23
|
+
reject(new CliError(EXIT.OUTSIDE, "TOOL_MISSING", `${cmd} is not installed.`));
|
|
24
|
+
else
|
|
25
|
+
reject(e);
|
|
26
|
+
});
|
|
27
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
28
|
+
if (opts.input !== undefined)
|
|
29
|
+
child.stdin?.end(opts.input);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function nodeDeps() {
|
|
33
|
+
return {
|
|
34
|
+
env: process.env,
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
home: homedir(),
|
|
37
|
+
platform: process.platform,
|
|
38
|
+
stdout: (t) => process.stdout.write(t),
|
|
39
|
+
stderr: (t) => process.stderr.write(t),
|
|
40
|
+
run: runProcess,
|
|
41
|
+
fetch: globalThis.fetch,
|
|
42
|
+
prompt: async (question) => {
|
|
43
|
+
if (!process.stdin.isTTY)
|
|
44
|
+
return null;
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
46
|
+
try {
|
|
47
|
+
return await rl.question(question);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
54
|
+
now: () => Date.now(),
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Deps } from "./context.js";
|
|
2
|
+
export type CliConfig = {
|
|
3
|
+
manager_url: string;
|
|
4
|
+
github_user: string;
|
|
5
|
+
broker_url?: string;
|
|
6
|
+
infra_dir?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const configPath: (home: string) => string;
|
|
9
|
+
/** `https://manager.<something>` and nothing else: no port, path, query, credentials (INCA-33). */
|
|
10
|
+
export declare function parseManagerUrl(raw: string): {
|
|
11
|
+
url: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function parseHttpsUrl(raw: string, what: string): string;
|
|
15
|
+
export declare const GITHUB_USER: RegExp;
|
|
16
|
+
export declare function readCliConfig(deps: Deps): Promise<CliConfig | null>;
|
|
17
|
+
export declare function requireCliConfig(deps: Deps): Promise<CliConfig>;
|
|
18
|
+
export declare function writeCliConfig(deps: Deps, c: CliConfig): Promise<void>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ~/.config/inca/cli.json: the manager address and the GitHub account, never a token (INCA-33, INCA-27).
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CliError, EXIT, usage } from "./context.js";
|
|
5
|
+
export const configPath = (home) => join(home, ".config", "inca", "cli.json");
|
|
6
|
+
/** `https://manager.<something>` and nothing else: no port, path, query, credentials (INCA-33). */
|
|
7
|
+
export function parseManagerUrl(raw) {
|
|
8
|
+
let u;
|
|
9
|
+
try {
|
|
10
|
+
u = new URL(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw usage(`${JSON.stringify(raw)} is not a URL; give https://manager.<domain>.`);
|
|
14
|
+
}
|
|
15
|
+
const labels = u.hostname.split(".");
|
|
16
|
+
const ok = u.protocol === "https:" &&
|
|
17
|
+
u.port === "" &&
|
|
18
|
+
u.username === "" &&
|
|
19
|
+
u.password === "" &&
|
|
20
|
+
(u.pathname === "/" || u.pathname === "") &&
|
|
21
|
+
u.search === "" &&
|
|
22
|
+
u.hash === "" &&
|
|
23
|
+
labels[0] === "manager" &&
|
|
24
|
+
labels.length >= 3 &&
|
|
25
|
+
labels.every((l) => l !== "");
|
|
26
|
+
if (!ok)
|
|
27
|
+
throw usage(`The manager URL must be https://manager.<domain>, not ${JSON.stringify(raw)}.`);
|
|
28
|
+
return { url: `https://${u.hostname}`, domain: labels.slice(1).join(".") };
|
|
29
|
+
}
|
|
30
|
+
export function parseHttpsUrl(raw, what) {
|
|
31
|
+
let u;
|
|
32
|
+
try {
|
|
33
|
+
u = new URL(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw usage(`${what} ${JSON.stringify(raw)} is not a URL.`);
|
|
37
|
+
}
|
|
38
|
+
if (u.protocol !== "https:" || u.username !== "" || u.password !== "" || u.search !== "" || u.hash !== "") {
|
|
39
|
+
throw usage(`${what} must be a plain https:// URL, not ${JSON.stringify(raw)}.`);
|
|
40
|
+
}
|
|
41
|
+
return u.origin + u.pathname.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
export const GITHUB_USER = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
44
|
+
export async function readCliConfig(deps) {
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = await readFile(configPath(deps.home), "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (e.code === "ENOENT")
|
|
51
|
+
return null;
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new CliError(EXIT.REFUSED, "CLI_CONFIG_INVALID", `${configPath(deps.home)} is not valid JSON.`, "Run inca login --manager https://manager.<domain> again.");
|
|
60
|
+
}
|
|
61
|
+
const r = raw;
|
|
62
|
+
if (typeof r.manager_url !== "string" || typeof r.github_user !== "string") {
|
|
63
|
+
throw new CliError(EXIT.REFUSED, "CLI_CONFIG_INVALID", `${configPath(deps.home)} lacks manager_url or github_user.`, "Run inca login again.");
|
|
64
|
+
}
|
|
65
|
+
// A hand-edited file is held to the same rule as the flag.
|
|
66
|
+
parseManagerUrl(r.manager_url);
|
|
67
|
+
if (!GITHUB_USER.test(r.github_user))
|
|
68
|
+
throw new CliError(EXIT.REFUSED, "CLI_CONFIG_INVALID", "github_user in cli.json is not a GitHub account name.");
|
|
69
|
+
return r;
|
|
70
|
+
}
|
|
71
|
+
export async function requireCliConfig(deps) {
|
|
72
|
+
const c = await readCliConfig(deps);
|
|
73
|
+
if (!c)
|
|
74
|
+
throw new CliError(EXIT.AUTH, "NOT_LOGGED_IN", "No manager is set.", "inca login --manager https://manager.<domain> --github-user <account>");
|
|
75
|
+
return c;
|
|
76
|
+
}
|
|
77
|
+
export async function writeCliConfig(deps, c) {
|
|
78
|
+
const dir = join(deps.home, ".config", "inca");
|
|
79
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
80
|
+
const path = configPath(deps.home);
|
|
81
|
+
await writeFile(path, `${JSON.stringify(c, null, 2)}\n`, { mode: 0o600 });
|
|
82
|
+
await chmod(path, 0o600);
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLI_VERSION = "1.0.1";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wujiland/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "inca: the command line for the whole life of an Inca app.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"inca": "dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
28
|
+
"prepack": "npm run build",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@wujiland/config": "1.0.0",
|
|
34
|
+
"blake3-wasm": "2.1.5",
|
|
35
|
+
"yaml": "2.9.1"
|
|
36
|
+
}
|
|
37
|
+
}
|