arketix 0.1.0 → 0.2.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 +4 -4
- package/dist/arketix.js +74 -50
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# arketix
|
|
2
2
|
|
|
3
|
-
The command line of
|
|
3
|
+
The command line of Arketix ERP (https://erp.arketix.cl), for founders and agents. Its commands come from Arketix ERP's operations, so a new operation is a new command with no new release.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm i -g arketix
|
|
@@ -16,7 +16,7 @@ arketix help crm opportunities
|
|
|
16
16
|
|
|
17
17
|
- **Flags come from each operation's input.** `ownerId` is `--owner-id`, enums are choices, objects and arrays take JSON, `--input '<json>'` passes the whole input. An operation with exactly one required id also takes it as one trailing word.
|
|
18
18
|
- **Output.** People get a table for a list and `key: value` lines otherwise. `--json` prints one line: the output, or `{"error":{"code","message","retryable"}}`.
|
|
19
|
-
- **Sign-in.** `login` keeps the token in the OS keychain. `ARKETIX_TOKEN` overrides it. `--env prod` (default) or `local` (http://localhost:3417); `ARKETIX_URL` points anywhere else.
|
|
20
|
-
- **Catalogue.**
|
|
19
|
+
- **Sign-in.** `login` keeps the token in the OS keychain. `ARKETIX_TOKEN` overrides it. `--env prod` (default, https://erp.arketix.cl) or `local` (http://localhost:3417); `ARKETIX_URL` points anywhere else. A token kept by 0.1.x for https://crm.admin.acredix.cl still signs in to prod; `logout` forgets both.
|
|
20
|
+
- **Catalogue.** Every command, and help for one, asks Arketix ERP for its list of operations. The list is cached per server version and CLI release in `~/.cache/arketix`, which only saves the `tools/list` call while the server's version is unchanged. Nothing falls back to it: when the server can't be reached or gives no usable answer, the command exits 9, and a refused token exits with its code. `arketix`, `arketix help` and the built-ins' help need no token and no connection, so they list only the built-ins; `arketix help crm` lists the CRM module's commands.
|
|
21
21
|
|
|
22
|
-
Exit codes: 0 ok, 1 other
|
|
22
|
+
Exit codes: 0 ok, 1 other error, 2 usage, 3 unauthenticated, 4 read-only token, 6 confirmation required, 7 not in catalogue, 8 rate limited, 9 connection lost (a write may have run), 10 forbidden, 12 input invalid.
|
package/dist/arketix.js
CHANGED
|
@@ -17266,6 +17266,17 @@ function _array(Class, element, params) {
|
|
|
17266
17266
|
...normalizeParams(params)
|
|
17267
17267
|
});
|
|
17268
17268
|
}
|
|
17269
|
+
function _custom(Class, fn, _params) {
|
|
17270
|
+
const norm = normalizeParams(_params);
|
|
17271
|
+
norm.abort ?? (norm.abort = true);
|
|
17272
|
+
const schema = new Class({
|
|
17273
|
+
type: "custom",
|
|
17274
|
+
check: "custom",
|
|
17275
|
+
fn,
|
|
17276
|
+
...norm
|
|
17277
|
+
});
|
|
17278
|
+
return schema;
|
|
17279
|
+
}
|
|
17269
17280
|
function _refine(Class, fn, _params) {
|
|
17270
17281
|
const schema = new Class({
|
|
17271
17282
|
type: "custom",
|
|
@@ -19569,6 +19580,9 @@ var ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
|
|
|
19569
19580
|
ZodType.init(inst, def);
|
|
19570
19581
|
inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);
|
|
19571
19582
|
});
|
|
19583
|
+
function custom(fn, _params) {
|
|
19584
|
+
return _custom(ZodCustom, fn ?? (() => true), _params);
|
|
19585
|
+
}
|
|
19572
19586
|
function refine(fn, _params = {}) {
|
|
19573
19587
|
return _refine(ZodCustom, fn, _params);
|
|
19574
19588
|
}
|
|
@@ -36485,12 +36499,13 @@ var fail9 = (code, message, retryable = false) => {
|
|
|
36485
36499
|
var exitCodes = EXIT_CODES;
|
|
36486
36500
|
var exitCodeFor = (code) => exitCodes[code] ?? 1;
|
|
36487
36501
|
// package.json
|
|
36488
|
-
var version2 = "0.
|
|
36502
|
+
var version2 = "0.2.0";
|
|
36489
36503
|
|
|
36490
36504
|
// src/version.ts
|
|
36491
36505
|
var VERSION = version2;
|
|
36492
36506
|
|
|
36493
36507
|
// src/crm.ts
|
|
36508
|
+
var Catalogue = object({ cli: literal(VERSION), server: string4(), tools: array2(custom()) });
|
|
36494
36509
|
var Output = record2(string4(), unknown2());
|
|
36495
36510
|
|
|
36496
36511
|
class ConnectionLost extends Error {
|
|
@@ -36508,10 +36523,12 @@ function noAnswer(url, error, read) {
|
|
|
36508
36523
|
retryable: false
|
|
36509
36524
|
};
|
|
36510
36525
|
}
|
|
36526
|
+
if (status === 403)
|
|
36527
|
+
return { code: "FORBIDDEN", message: `${url} refused this token access.`, retryable: false };
|
|
36511
36528
|
if (status === 429)
|
|
36512
36529
|
return { code: "RATE_LIMITED", message: `${url} is rate limiting this token. Wait and retry.`, retryable: true };
|
|
36513
36530
|
const lost = error instanceof ConnectionLost || error instanceof Error && error.cause instanceof ConnectionLost;
|
|
36514
|
-
const what = lost ? `The connection to ${url} failed` : `${url} gave no answer (${error instanceof Error ? error.message : String(error)})`;
|
|
36531
|
+
const what = lost ? `The connection to ${url} failed` : `${url} gave no usable answer (${error instanceof Error ? error.message : String(error)})`;
|
|
36515
36532
|
return read ? { code: "CONNECTION_LOST", message: `${what}. Nothing changed: retry.`, retryable: true } : { code: "CONNECTION_LOST", message: `${what}, so the call may have run. Check the current state before trying again.`, retryable: false };
|
|
36516
36533
|
}
|
|
36517
36534
|
function toolError(text) {
|
|
@@ -36535,40 +36552,41 @@ async function connect({ url, token, fetch: fetch2, catalogues }) {
|
|
|
36535
36552
|
throw new ConnectionLost(error instanceof Error ? error.message : String(error));
|
|
36536
36553
|
}
|
|
36537
36554
|
},
|
|
36538
|
-
requestInit: { headers: { authorization: `Bearer ${token}
|
|
36555
|
+
requestInit: { headers: { ...token && { authorization: `Bearer ${token}` }, "x-arketix-channel": "cli" } }
|
|
36539
36556
|
});
|
|
36540
36557
|
const answered = async (request, read) => {
|
|
36541
36558
|
try {
|
|
36542
36559
|
return await request;
|
|
36543
36560
|
} catch (error) {
|
|
36544
|
-
|
|
36561
|
+
const failure = noAnswer(url, error, read);
|
|
36562
|
+
if (!token && failure.code === "UNAUTHENTICATED")
|
|
36563
|
+
notSignedIn(url);
|
|
36564
|
+
throw new CliFailure(failure);
|
|
36545
36565
|
}
|
|
36546
36566
|
};
|
|
36547
36567
|
await answered(client.connect(transport), true);
|
|
36548
|
-
const
|
|
36568
|
+
const server = client.getServerVersion()?.version ?? "";
|
|
36549
36569
|
return {
|
|
36550
36570
|
catalogue: async () => {
|
|
36551
36571
|
const all = await catalogues.read();
|
|
36552
|
-
const cached = all[url];
|
|
36553
|
-
if (cached
|
|
36572
|
+
const cached = Catalogue.safeParse(all[url]).data;
|
|
36573
|
+
if (cached?.server === server)
|
|
36554
36574
|
return cached;
|
|
36555
|
-
const fresh = {
|
|
36575
|
+
const fresh = { cli: VERSION, server, tools: (await answered(client.listTools(), true)).tools };
|
|
36556
36576
|
await catalogues.write({ ...all, [url]: fresh });
|
|
36557
36577
|
return fresh;
|
|
36558
36578
|
},
|
|
36559
36579
|
call: async (name, args, { read }) => {
|
|
36560
|
-
const
|
|
36580
|
+
const unusable = (why) => new CliFailure(noAnswer(url, new Error(why), read));
|
|
36581
|
+
const result = await answered(client.callTool({ name, arguments: args }).catch((error) => {
|
|
36582
|
+
throw error instanceof ProtocolError ? unusable(error.message) : error;
|
|
36583
|
+
}), read);
|
|
36561
36584
|
if (result.isError)
|
|
36562
36585
|
throw new CliFailure(toolError(textOf(result.content)));
|
|
36563
|
-
|
|
36564
|
-
|
|
36565
|
-
|
|
36566
|
-
|
|
36567
|
-
const value = JSON.parse(text);
|
|
36568
|
-
return Output.safeParse(value).data ?? { value };
|
|
36569
|
-
} catch {
|
|
36570
|
-
return { text };
|
|
36571
|
-
}
|
|
36586
|
+
const output = Output.safeParse(result.structuredContent);
|
|
36587
|
+
if (!output.success)
|
|
36588
|
+
throw unusable(`${name}: no structured content object`);
|
|
36589
|
+
return output.data;
|
|
36572
36590
|
},
|
|
36573
36591
|
close: () => client.close()
|
|
36574
36592
|
};
|
|
@@ -36604,26 +36622,31 @@ function human(output, columns = Infinity) {
|
|
|
36604
36622
|
}
|
|
36605
36623
|
|
|
36606
36624
|
// src/cli.ts
|
|
36607
|
-
var URLS = { prod: "https://
|
|
36625
|
+
var URLS = { prod: "https://erp.arketix.cl", local: "http://localhost:3417" };
|
|
36608
36626
|
var isEnv = (value) => (value in URLS);
|
|
36609
|
-
|
|
36610
|
-
|
|
36611
|
-
|
|
36612
|
-
|
|
36613
|
-
|
|
36614
|
-
|
|
36615
|
-
|
|
36616
|
-
|
|
36617
|
-
|
|
36618
|
-
|
|
36627
|
+
var OLD_PROD = "https://crm.admin.acredix.cl";
|
|
36628
|
+
function preParse(argv) {
|
|
36629
|
+
let env;
|
|
36630
|
+
const words = [];
|
|
36631
|
+
for (let index = 0;index < argv.length; index++) {
|
|
36632
|
+
const token = argv[index];
|
|
36633
|
+
if (token === "--env")
|
|
36634
|
+
env = argv[++index];
|
|
36635
|
+
else if (token.startsWith("--env="))
|
|
36636
|
+
env = token.slice("--env=".length);
|
|
36637
|
+
else if (!token.startsWith("-"))
|
|
36638
|
+
words.push(token);
|
|
36639
|
+
}
|
|
36640
|
+
return { env, words };
|
|
36641
|
+
}
|
|
36642
|
+
function targetOf(env, io) {
|
|
36619
36643
|
if (!isEnv(env))
|
|
36620
36644
|
return fail9("USAGE", "--env is prod or local; ARKETIX_URL points anywhere else.");
|
|
36621
36645
|
const url = new URL(io.env.ARKETIX_URL ?? URLS[env]).origin;
|
|
36622
|
-
return { url, token: io.env.ARKETIX_TOKEN?.trim() || io.secrets.get(url) };
|
|
36646
|
+
return { url, token: io.env.ARKETIX_TOKEN?.trim() || io.secrets.get(url) || (url === URLS.prod ? io.secrets.get(OLD_PROD) : undefined) };
|
|
36623
36647
|
}
|
|
36624
36648
|
var opIdOf = (tool) => tool.name.replaceAll("_", ".");
|
|
36625
36649
|
var kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
36626
|
-
var operationsOf = (tools) => tools.filter((tool) => !tool.description?.startsWith("Deprecated:"));
|
|
36627
36650
|
var Property = looseObject({
|
|
36628
36651
|
type: union2([string4(), array2(string4())]).optional(),
|
|
36629
36652
|
anyOf: array2(looseObject({ type: string4().optional() })).optional(),
|
|
@@ -36656,7 +36679,7 @@ var InputSchema = object({ properties: record2(string4(), Property).optional(),
|
|
|
36656
36679
|
var print = (run, value, text) => run.io.stdout(`${run.json ? JSON.stringify(value) : text}
|
|
36657
36680
|
`);
|
|
36658
36681
|
var needCrm = (run) => run.crm ?? notSignedIn(run.url);
|
|
36659
|
-
var operation = (run, id) =>
|
|
36682
|
+
var operation = (run, id) => run.catalogue.find((tool) => opIdOf(tool) === id) ?? fail9("NOT_IN_CATALOGUE", `${id} is not an operation of ${run.url}. \`arketix help\` lists them.`);
|
|
36660
36683
|
async function callOperation(run, tool, input, confirmed) {
|
|
36661
36684
|
if (tool.annotations?.destructiveHint && !confirmed) {
|
|
36662
36685
|
fail9("CONFIRMATION_REQUIRED", `${opIdOf(tool)} deletes or cancels. Nothing changed: repeat the command with --confirm to go ahead.`);
|
|
@@ -36695,7 +36718,7 @@ function operationCommand(run, tool, word) {
|
|
|
36695
36718
|
}
|
|
36696
36719
|
function operationCommands(run) {
|
|
36697
36720
|
const root = { children: new Map };
|
|
36698
|
-
for (const tool of
|
|
36721
|
+
for (const tool of run.catalogue) {
|
|
36699
36722
|
let node = root;
|
|
36700
36723
|
for (const word of opIdOf(tool).split(".").map(kebab)) {
|
|
36701
36724
|
const next = node.children.get(word) ?? { children: new Map };
|
|
@@ -36711,7 +36734,7 @@ var JsonFlag = Setting("json")({
|
|
|
36711
36734
|
flag: Boolean6("json").pipe(withDescription3("One line of JSON on stdout, errors included"), withDefault4(false))
|
|
36712
36735
|
});
|
|
36713
36736
|
var EnvFlag = Setting("env")({
|
|
36714
|
-
flag: Literals3("env", ["prod", "local"]).pipe(withDescription3("Which
|
|
36737
|
+
flag: Literals3("env", ["prod", "local"]).pipe(withDescription3("Which server: prod is erp.arketix.cl, local is localhost:3417. ARKETIX_URL points anywhere else"), withDefault4("prod"))
|
|
36715
36738
|
});
|
|
36716
36739
|
var Me = object({
|
|
36717
36740
|
member: object({ name: string4(), email: string4(), role: string4() }),
|
|
@@ -36734,8 +36757,10 @@ function builtIns(run) {
|
|
|
36734
36757
|
await crm.close();
|
|
36735
36758
|
}
|
|
36736
36759
|
})));
|
|
36737
|
-
const logout = make12("logout").pipe(withDescription4("Forget the token for this
|
|
36760
|
+
const logout = make12("logout").pipe(withDescription4("Forget the token for this server."), withHandler(handle(async () => {
|
|
36738
36761
|
run.io.secrets.delete(run.url);
|
|
36762
|
+
if (run.url === URLS.prod)
|
|
36763
|
+
run.io.secrets.delete(OLD_PROD);
|
|
36739
36764
|
print(run, { signedOut: run.url }, `Signed out of ${run.url}.`);
|
|
36740
36765
|
})));
|
|
36741
36766
|
const whoami = make12("whoami").pipe(withDescription4("Who the token belongs to, and whether it may write."), withHandler(handle(async () => {
|
|
@@ -36757,28 +36782,27 @@ function builtIns(run) {
|
|
|
36757
36782
|
}
|
|
36758
36783
|
var readInputFile = (io, path) => io.readFile(path).catch(() => fail9("USAGE", `Can't read ${path}.`));
|
|
36759
36784
|
var BUILT_IN_ONLY = new Set(["login", "logout", "version"]);
|
|
36785
|
+
var BUILT_INS = new Set([...BUILT_IN_ONLY, "whoami", "help", "call"]);
|
|
36760
36786
|
async function runCli(args, io) {
|
|
36761
36787
|
const json = args.includes("--json");
|
|
36762
|
-
const
|
|
36763
|
-
const
|
|
36788
|
+
const { env = "prod", words } = preParse(args);
|
|
36789
|
+
const helpWord = words[0] === "help";
|
|
36790
|
+
const argv = helpWord ? [...args.toSpliced(args.indexOf("help"), 1), "--help"] : [...args];
|
|
36791
|
+
const command = words[helpWord ? 1 : 0];
|
|
36792
|
+
const wantsHelp = command === undefined || argv.includes("--help") || argv.includes("-h");
|
|
36793
|
+
const builtInHelp = wantsHelp && (command === undefined || BUILT_INS.has(command));
|
|
36764
36794
|
let crm;
|
|
36765
36795
|
try {
|
|
36766
|
-
const { url, token } = targetOf(
|
|
36767
|
-
let catalogue =
|
|
36768
|
-
if (!BUILT_IN_ONLY.has(
|
|
36769
|
-
if (token)
|
|
36770
|
-
try {
|
|
36771
|
-
crm = await connect({ url, token, fetch: io.fetch, catalogues: io.catalogues });
|
|
36772
|
-
catalogue = (await crm.catalogue()).tools;
|
|
36773
|
-
} catch (error) {
|
|
36774
|
-
if (!wantsHelp)
|
|
36775
|
-
throw error;
|
|
36776
|
-
}
|
|
36777
|
-
} else if (!wantsHelp)
|
|
36796
|
+
const { url, token } = targetOf(env, io);
|
|
36797
|
+
let catalogue = [];
|
|
36798
|
+
if (!builtInHelp && !BUILT_IN_ONLY.has(command ?? "") && !argv.includes("--version")) {
|
|
36799
|
+
if (!token && !wantsHelp)
|
|
36778
36800
|
notSignedIn(url);
|
|
36801
|
+
crm = await connect({ url, token, fetch: io.fetch, catalogues: io.catalogues });
|
|
36802
|
+
catalogue = (await crm.catalogue()).tools;
|
|
36779
36803
|
}
|
|
36780
36804
|
const run = { io, json, url, crm, catalogue };
|
|
36781
|
-
const cli = make12("arketix").pipe(withDescription4(`
|
|
36805
|
+
const cli = make12("arketix").pipe(withDescription4(`Arketix ERP (${url}) for people and agents. Commands come from its operations: ` + "`arketix help crm`, `arketix help metrics`, `arketix help settings`, `arketix help account`. " + "Exit codes: 0 ok, 1 other error, 2 usage, 3 unauthenticated, 4 read-only token, 6 confirmation required, " + "7 not in catalogue, 8 rate limited, 9 connection lost, 10 forbidden, 12 input invalid."), withSubcommands([...builtIns(run), ...operationCommands(run)]), withGlobalFlags([JsonFlag, EnvFlag]));
|
|
36782
36806
|
const exit = await runPromiseExit2(runWith2(cli, { version: VERSION, renderErrors: false })(argv).pipe(provide3(services(io, json))));
|
|
36783
36807
|
if (isSuccess3(exit))
|
|
36784
36808
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arketix",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command line for
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Command line for Arketix ERP: every operation, for founders and agents.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|