@michinarai/cli 0.0.0-stage → 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 +202 -0
- package/README.md +150 -2
- package/bin/run.js +4 -0
- package/dist/api/client.d.ts +24 -0
- package/dist/api/client.js +56 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/tool-flag.d.ts +9 -0
- package/dist/api/tool-flag.js +15 -0
- package/dist/api/tool-flag.js.map +1 -0
- package/dist/api/tools.d.ts +17 -0
- package/dist/api/tools.js +39 -0
- package/dist/api/tools.js.map +1 -0
- package/dist/auth/loopback.d.ts +11 -0
- package/dist/auth/loopback.js +54 -0
- package/dist/auth/loopback.js.map +1 -0
- package/dist/auth/oauth.d.ts +52 -0
- package/dist/auth/oauth.js +101 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/session.d.ts +7 -0
- package/dist/auth/session.js +41 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/base-command.d.ts +29 -0
- package/dist/base-command.js +65 -0
- package/dist/base-command.js.map +1 -0
- package/dist/commands/build.d.ts +9 -0
- package/dist/commands/build.js +34 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.js +55 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +63 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.js +37 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +19 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/tools/list.d.ts +8 -0
- package/dist/commands/tools/list.js +23 -0
- package/dist/commands/tools/list.js.map +1 -0
- package/dist/commands/tools/push.d.ts +11 -0
- package/dist/commands/tools/push.js +53 -0
- package/dist/commands/tools/push.js.map +1 -0
- package/dist/commands/tools/register.d.ts +13 -0
- package/dist/commands/tools/register.js +66 -0
- package/dist/commands/tools/register.js.map +1 -0
- package/dist/commands/tools/tokens/create.d.ts +12 -0
- package/dist/commands/tools/tokens/create.js +26 -0
- package/dist/commands/tools/tokens/create.js.map +1 -0
- package/dist/commands/tools/tokens/list.d.ts +8 -0
- package/dist/commands/tools/tokens/list.js +13 -0
- package/dist/commands/tools/tokens/list.js.map +1 -0
- package/dist/commands/tools/tokens/revoke.d.ts +11 -0
- package/dist/commands/tools/tokens/revoke.js +18 -0
- package/dist/commands/tools/tokens/revoke.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +27 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands.d.ts +26 -0
- package/dist/commands.js +29 -0
- package/dist/commands.js.map +1 -0
- package/dist/generated/api.d.ts +1499 -0
- package/dist/generated/api.js +6 -0
- package/dist/generated/api.js.map +1 -0
- package/dist/lib/credentials.d.ts +8 -0
- package/dist/lib/credentials.js +45 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/deps.d.ts +12 -0
- package/dist/lib/deps.js +23 -0
- package/dist/lib/deps.js.map +1 -0
- package/dist/lib/digest.d.ts +10 -0
- package/dist/lib/digest.js +37 -0
- package/dist/lib/digest.js.map +1 -0
- package/dist/lib/errors.d.ts +26 -0
- package/dist/lib/errors.js +39 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/http.d.ts +2 -0
- package/dist/lib/http.js +11 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/lock.d.ts +4 -0
- package/dist/lib/lock.js +58 -0
- package/dist/lib/lock.js.map +1 -0
- package/dist/lib/output.d.ts +21 -0
- package/dist/lib/output.js +44 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/plan.d.ts +13 -0
- package/dist/lib/plan.js +10 -0
- package/dist/lib/plan.js.map +1 -0
- package/dist/lib/project.d.ts +8 -0
- package/dist/lib/project.js +37 -0
- package/dist/lib/project.js.map +1 -0
- package/package.json +59 -4
- package/templates/tool/.claude/skills/narai-tool/SKILL.md +16 -0
- package/templates/tool/AGENTS.md +30 -0
- package/templates/tool/narai.tool.json +25 -0
- package/templates/tool/package.json +14 -0
- package/templates/tool/scripts/build.mjs +12 -0
- package/templates/tool/src/index.html +19 -0
- package/templates/tool/src/tool.css +2 -0
- package/templates/tool/src/tool.js +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/generated/api.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const keychainService: (naraiUrl: string) => string;
|
|
2
|
+
export interface CredentialStore {
|
|
3
|
+
get(naraiUrl: string): Promise<string | null>;
|
|
4
|
+
set(naraiUrl: string, token: string): Promise<void>;
|
|
5
|
+
/** Whether there was one to delete. */
|
|
6
|
+
delete(naraiUrl: string): Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export declare const keychain: CredentialStore;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NaraiError } from "./errors.js";
|
|
2
|
+
/** The one place a person's Narai session lives: the OS keychain (macOS Keychain, Windows Credential Manager, the
|
|
3
|
+
* Secret Service on Linux), one entry per Narai host, holding the refresh token. No file fallback: a machine without
|
|
4
|
+
* a keychain (CI) uses a token in NARAI_TOKEN. */
|
|
5
|
+
const ACCOUNT = "refresh_token";
|
|
6
|
+
export const keychainService = (naraiUrl) => `ai.michinar.narai-cli.${new URL(naraiUrl).host}`;
|
|
7
|
+
class KeychainUnavailableError extends NaraiError {
|
|
8
|
+
constructor(cause) {
|
|
9
|
+
super(`the OS keychain isn't available (${cause instanceof Error ? cause.message : String(cause)}); on a machine without one, set NARAI_TOKEN`, "keychain_unavailable");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Loaded on first use: CI runs with NARAI_TOKEN and never loads the native module. */
|
|
13
|
+
async function entry(naraiUrl) {
|
|
14
|
+
let keyring;
|
|
15
|
+
try {
|
|
16
|
+
keyring = await import("@napi-rs/keyring");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new KeychainUnavailableError(error);
|
|
20
|
+
}
|
|
21
|
+
return new keyring.AsyncEntry(keychainService(naraiUrl), ACCOUNT);
|
|
22
|
+
}
|
|
23
|
+
async function guarded(action) {
|
|
24
|
+
try {
|
|
25
|
+
return await action();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new KeychainUnavailableError(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export const keychain = {
|
|
32
|
+
async get(naraiUrl) {
|
|
33
|
+
const e = await entry(naraiUrl);
|
|
34
|
+
return (await guarded(() => e.getPassword())) ?? null;
|
|
35
|
+
},
|
|
36
|
+
async set(naraiUrl, token) {
|
|
37
|
+
const e = await entry(naraiUrl);
|
|
38
|
+
await guarded(() => e.setPassword(token));
|
|
39
|
+
},
|
|
40
|
+
async delete(naraiUrl) {
|
|
41
|
+
const e = await entry(naraiUrl);
|
|
42
|
+
return guarded(() => e.deleteCredential());
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;mDAEmD;AACnD,MAAM,OAAO,GAAG,eAAe,CAAC;AAEhC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,yBAAyB,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;AAS/G,MAAM,wBAAyB,SAAQ,UAAU;IAC/C,YAAY,KAAc;QACxB,KAAK,CACH,oCAAoC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EACxI,sBAAsB,CACvB,CAAC;IACJ,CAAC;CACF;AAED,uFAAuF;AACvF,KAAK,UAAU,KAAK,CAAC,QAAgB;IACnC,IAAI,OAA0C,CAAC;IAC/C,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AACpE,CAAC;AAED,KAAK,UAAU,OAAO,CAAI,MAAwB;IAChD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,QAAQ,GAAoB;IACvC,KAAK,CAAC,GAAG,CAAC,QAAQ;QAChB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC;IACxD,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK;QACvB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,QAAQ;QACnB,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CredentialStore } from "./credentials.js";
|
|
2
|
+
/** What touches the person's machine beyond files: swapped in tests. */
|
|
3
|
+
export declare const deps: {
|
|
4
|
+
credentials: CredentialStore;
|
|
5
|
+
stdinIsTTY: () => boolean;
|
|
6
|
+
stdoutIsTTY: () => boolean;
|
|
7
|
+
confirm: (message: string) => Promise<boolean>;
|
|
8
|
+
sleep: (ms: number) => Promise<void>;
|
|
9
|
+
now: () => number;
|
|
10
|
+
/** Opens the sign-in page in the person's browser; the address is printed too, so a failure here is no matter. */
|
|
11
|
+
openBrowser: (url: string) => void;
|
|
12
|
+
};
|
package/dist/lib/deps.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import confirm from "@inquirer/confirm";
|
|
3
|
+
import { keychain } from "./credentials.js";
|
|
4
|
+
/** What touches the person's machine beyond files: swapped in tests. */
|
|
5
|
+
export const deps = {
|
|
6
|
+
credentials: keychain,
|
|
7
|
+
stdinIsTTY: () => Boolean(process.stdin.isTTY),
|
|
8
|
+
stdoutIsTTY: () => Boolean(process.stdout.isTTY),
|
|
9
|
+
confirm: (message) => confirm({ message, default: false }),
|
|
10
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
11
|
+
now: () => Date.now(),
|
|
12
|
+
/** Opens the sign-in page in the person's browser; the address is printed too, so a failure here is no matter. */
|
|
13
|
+
openBrowser: (url) => {
|
|
14
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
15
|
+
try {
|
|
16
|
+
spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => undefined).unref();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// printed already
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=deps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/lib/deps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,OAAO,MAAM,mBAAmB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAwB,MAAM,kBAAkB,CAAC;AAElE,wEAAwE;AACxE,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,WAAW,EAAE,QAA2B;IACxC,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;IAC9C,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;IAChD,OAAO,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAClE,KAAK,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9E,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;IACrB,kHAAkH;IAClH,WAAW,EAAE,CAAC,GAAW,EAAQ,EAAE;QACjC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjK,IAAI,CAAC;YACH,KAAK,CAAC,OAAO,EAAE,IAAgB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,CAAC;QAC7G,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;IACH,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface FileDigest {
|
|
2
|
+
path: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
size: number;
|
|
5
|
+
}
|
|
6
|
+
/** Every file of a release, with its sha256 and size, sorted by path. The manifest itself isn't one of them. */
|
|
7
|
+
export declare function digestFiles(dir: string): Promise<FileDigest[]>;
|
|
8
|
+
/** sha256 over the file list's canonical JSON (RFC 8785; for these strings and integers: keys sorted, no space), as
|
|
9
|
+
* Narai's registry computes it. */
|
|
10
|
+
export declare function bundleDigest(files: readonly FileDigest[]): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
import { MANIFEST } from "./project.js";
|
|
5
|
+
/** Paths compared by code point, as Narai's registry sorts them (Python's), not by UTF-16 unit. */
|
|
6
|
+
const byCodePoint = (a, b) => {
|
|
7
|
+
const points = (s) => Array.from(s, (c) => c.codePointAt(0) ?? 0);
|
|
8
|
+
const [x, y] = [points(a), points(b)];
|
|
9
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) {
|
|
10
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
11
|
+
if (d !== 0)
|
|
12
|
+
return d;
|
|
13
|
+
}
|
|
14
|
+
return x.length - y.length;
|
|
15
|
+
};
|
|
16
|
+
/** Every file of a release, with its sha256 and size, sorted by path. The manifest itself isn't one of them. */
|
|
17
|
+
export async function digestFiles(dir) {
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const entry of await readdir(dir, { recursive: true, withFileTypes: true })) {
|
|
20
|
+
if (!entry.isFile())
|
|
21
|
+
continue;
|
|
22
|
+
const full = join(entry.parentPath, entry.name);
|
|
23
|
+
const path = relative(dir, full).split(sep).join("/");
|
|
24
|
+
if (path === MANIFEST)
|
|
25
|
+
continue;
|
|
26
|
+
const bytes = await readFile(full);
|
|
27
|
+
files.push({ path, sha256: createHash("sha256").update(bytes).digest("hex"), size: bytes.length });
|
|
28
|
+
}
|
|
29
|
+
return files.sort((a, b) => byCodePoint(a.path, b.path));
|
|
30
|
+
}
|
|
31
|
+
/** sha256 over the file list's canonical JSON (RFC 8785; for these strings and integers: keys sorted, no space), as
|
|
32
|
+
* Narai's registry computes it. */
|
|
33
|
+
export function bundleDigest(files) {
|
|
34
|
+
const canonical = JSON.stringify(files.map(({ path, sha256, size }) => ({ path, sha256, size })));
|
|
35
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/lib/digest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAIxC,mGAAmG;AACnG,MAAM,WAAW,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;IACnD,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1E,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;AAC7B,CAAC,CAAC;AAEF,gHAAgH;AAChH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjF,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;YAAE,SAAS;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtD,IAAI,IAAI,KAAK,QAAQ;YAAE,SAAS;QAChC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACrG,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;oCACoC;AACpC,MAAM,UAAU,YAAY,CAAC,KAA4B;IACvD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAClG,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The exit codes every command answers with: a contract scripts, CI and agents branch on. */
|
|
2
|
+
export declare const ExitCode: {
|
|
3
|
+
readonly ok: 0;
|
|
4
|
+
readonly error: 1;
|
|
5
|
+
/** A flag or argument the command doesn't take. */
|
|
6
|
+
readonly usage: 2;
|
|
7
|
+
/** Not signed in, or the session or token was refused. */
|
|
8
|
+
readonly auth: 3;
|
|
9
|
+
/** Only with `--detailed-exitcode`: the plan has changes (0 then means none). */
|
|
10
|
+
readonly changes: 2;
|
|
11
|
+
};
|
|
12
|
+
/** Every failure the CLI reports on purpose. `code` is stable and machine-readable (`--json`); `message` is for
|
|
13
|
+
* people and never carries a secret. */
|
|
14
|
+
export declare class NaraiError extends Error {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly exitCode: number;
|
|
17
|
+
readonly details: Record<string, unknown>;
|
|
18
|
+
constructor(message: string, code: string, exitCode?: number, details?: Record<string, unknown>);
|
|
19
|
+
}
|
|
20
|
+
export declare class AuthError extends NaraiError {
|
|
21
|
+
constructor(message: string, code?: string);
|
|
22
|
+
}
|
|
23
|
+
/** A confirmation was needed, and `--no-input` (or no terminal) ruled out asking. */
|
|
24
|
+
export declare class NeedsInputError extends NaraiError {
|
|
25
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The exit codes every command answers with: a contract scripts, CI and agents branch on. */
|
|
2
|
+
export const ExitCode = {
|
|
3
|
+
ok: 0,
|
|
4
|
+
error: 1,
|
|
5
|
+
/** A flag or argument the command doesn't take. */
|
|
6
|
+
usage: 2,
|
|
7
|
+
/** Not signed in, or the session or token was refused. */
|
|
8
|
+
auth: 3,
|
|
9
|
+
/** Only with `--detailed-exitcode`: the plan has changes (0 then means none). */
|
|
10
|
+
changes: 2,
|
|
11
|
+
};
|
|
12
|
+
/** Every failure the CLI reports on purpose. `code` is stable and machine-readable (`--json`); `message` is for
|
|
13
|
+
* people and never carries a secret. */
|
|
14
|
+
export class NaraiError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
exitCode;
|
|
17
|
+
details;
|
|
18
|
+
constructor(message, code, exitCode = ExitCode.error, details = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
this.details = details;
|
|
23
|
+
this.name = "NaraiError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class AuthError extends NaraiError {
|
|
27
|
+
constructor(message, code = "not_signed_in") {
|
|
28
|
+
super(message, code, ExitCode.auth);
|
|
29
|
+
this.name = "AuthError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A confirmation was needed, and `--no-input` (or no terminal) ruled out asking. */
|
|
33
|
+
export class NeedsInputError extends NaraiError {
|
|
34
|
+
constructor(message, details = {}) {
|
|
35
|
+
super(message, "needs_input", ExitCode.error, details);
|
|
36
|
+
this.name = "NeedsInputError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,CAAC;IACR,mDAAmD;IACnD,KAAK,EAAE,CAAC;IACR,0DAA0D;IAC1D,IAAI,EAAE,CAAC;IACP,iFAAiF;IACjF,OAAO,EAAE,CAAC;CACF,CAAC;AAEX;yCACyC;AACzC,MAAM,OAAO,UAAW,SAAQ,KAAK;IAGxB;IACA;IACA;IAJX,YACE,OAAe,EACN,IAAY,EACZ,WAAmB,QAAQ,CAAC,KAAK,EACjC,UAAmC,EAAE;QAE9C,KAAK,CAAC,OAAO,CAAC,CAAC;QAJN,SAAI,GAAJ,IAAI,CAAQ;QACZ,aAAQ,GAAR,QAAQ,CAAyB;QACjC,YAAO,GAAP,OAAO,CAA8B;QAG9C,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,SAAU,SAAQ,UAAU;IACvC,YAAY,OAAe,EAAE,IAAI,GAAG,eAAe;QACjD,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,OAAO,eAAgB,SAAQ,UAAU;IAC7C,YAAY,OAAe,EAAE,UAAmC,EAAE;QAChE,KAAK,CAAC,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF"}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NaraiError } from "./errors.js";
|
|
2
|
+
/** Every request the CLI makes: no redirects followed (a token never goes where it wasn't sent), a timeout. */
|
|
3
|
+
export async function send(url, init = {}) {
|
|
4
|
+
try {
|
|
5
|
+
return await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(30_000), ...init });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new NaraiError(`couldn't reach ${new URL(url).host}: ${error.message}`, "network");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,+GAA+G;AAC/G,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAW,EAAE,OAAoB,EAAE;IAC5D,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IAChG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,UAAU,CAAC,kBAAkB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,KAAM,KAAe,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACtG,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Runs `action` holding the lock file at `path`, shared by every `narai` on this machine (two refreshing one
|
|
2
|
+
* rotating token at once would leave one of them holding a dead one). The file names its owner's pid; a lock whose
|
|
3
|
+
* owner is gone is taken over. */
|
|
4
|
+
export declare function withFileLock<T>(path: string, action: () => Promise<T>): Promise<T>;
|
package/dist/lib/lock.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { deps } from "./deps.js";
|
|
5
|
+
const POLL_MS = 100;
|
|
6
|
+
function running(pid) {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return error.code === "EPERM";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Runs `action` holding the lock file at `path`, shared by every `narai` on this machine (two refreshing one
|
|
16
|
+
* rotating token at once would leave one of them holding a dead one). The file names its owner's pid; a lock whose
|
|
17
|
+
* owner is gone is taken over. */
|
|
18
|
+
export async function withFileLock(path, action) {
|
|
19
|
+
await mkdir(dirname(path), { recursive: true });
|
|
20
|
+
// written whole, then linked into place: link fails when the lock exists, and no one sees a lock without its owner
|
|
21
|
+
const mine = `${path}.${process.pid}.${randomUUID()}`;
|
|
22
|
+
await writeFile(mine, String(process.pid));
|
|
23
|
+
try {
|
|
24
|
+
for (;;) {
|
|
25
|
+
try {
|
|
26
|
+
await link(mine, path);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "EEXIST")
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
let owner;
|
|
34
|
+
try {
|
|
35
|
+
owner = Number(await readFile(path, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === "ENOENT")
|
|
39
|
+
continue;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (Number.isInteger(owner) && owner > 0 && running(owner))
|
|
43
|
+
await deps.sleep(POLL_MS);
|
|
44
|
+
else
|
|
45
|
+
await rm(path, { force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await rm(mine, { force: true });
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return await action();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await rm(path, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/lib/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,OAAO,GAAG,GAAG,CAAC;AAEpB,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AAED;;mCAEmC;AACnC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAI,IAAY,EAAE,MAAwB;IAC1E,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,mHAAmH;IACnH,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,EAAE,CAAC;IACtD,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,CAAC;QACH,SAAS,CAAC;YACR,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;gBACvB,MAAM;YACR,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,MAAM,KAAK,CAAC;YACtE,CAAC;YACD,IAAI,KAAa,CAAC;YAClB,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC/C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,SAAS;gBACjE,MAAM,KAAK,CAAC;YACd,CAAC;YACD,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC;gBAAE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;;gBACjF,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,EAAE,CAAC;IACxB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const clean: (text: string) => string;
|
|
2
|
+
export interface OutputOptions {
|
|
3
|
+
json: boolean;
|
|
4
|
+
stdout?: NodeJS.WritableStream;
|
|
5
|
+
stderr?: NodeJS.WritableStream;
|
|
6
|
+
}
|
|
7
|
+
/** Where everything a command says goes: stdout carries only the result (text, or one JSON document with `--json`);
|
|
8
|
+
* progress, prompts and errors go to stderr. */
|
|
9
|
+
export declare class Output {
|
|
10
|
+
readonly json: boolean;
|
|
11
|
+
private readonly stdout;
|
|
12
|
+
private readonly stderr;
|
|
13
|
+
constructor(options: OutputOptions);
|
|
14
|
+
/** The command's answer: `data` as JSON with `--json`, else the lines `render` gives. */
|
|
15
|
+
result(data: unknown, render: () => string | readonly string[]): void;
|
|
16
|
+
/** For people; silent with `--json`. */
|
|
17
|
+
progress(message: string): void;
|
|
18
|
+
/** What a person must see to act (where to sign in, what they confirm): written even with `--json`. */
|
|
19
|
+
prompt(message: string): void;
|
|
20
|
+
error(code: string, message: string, details?: Record<string, unknown>): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// What a server or a file says is printed only after control characters are taken out, so nothing a server returns
|
|
2
|
+
// can move the cursor, rewrite a line or set a terminal's title.
|
|
3
|
+
// eslint-disable-next-line no-control-regex -- the control characters are what's removed
|
|
4
|
+
const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
|
|
5
|
+
export const clean = (text) => text.replace(CONTROL, "");
|
|
6
|
+
/** Where everything a command says goes: stdout carries only the result (text, or one JSON document with `--json`);
|
|
7
|
+
* progress, prompts and errors go to stderr. */
|
|
8
|
+
export class Output {
|
|
9
|
+
json;
|
|
10
|
+
stdout;
|
|
11
|
+
stderr;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.json = options.json;
|
|
14
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
15
|
+
this.stderr = options.stderr ?? process.stderr;
|
|
16
|
+
}
|
|
17
|
+
/** The command's answer: `data` as JSON with `--json`, else the lines `render` gives. */
|
|
18
|
+
result(data, render) {
|
|
19
|
+
if (this.json) {
|
|
20
|
+
this.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const text = render();
|
|
24
|
+
const lines = typeof text === "string" ? [text] : text;
|
|
25
|
+
if (lines.length > 0)
|
|
26
|
+
this.stdout.write(`${lines.map(clean).join("\n")}\n`);
|
|
27
|
+
}
|
|
28
|
+
/** For people; silent with `--json`. */
|
|
29
|
+
progress(message) {
|
|
30
|
+
if (!this.json)
|
|
31
|
+
this.stderr.write(`${clean(message)}\n`);
|
|
32
|
+
}
|
|
33
|
+
/** What a person must see to act (where to sign in, what they confirm): written even with `--json`. */
|
|
34
|
+
prompt(message) {
|
|
35
|
+
this.stderr.write(`${clean(message)}\n`);
|
|
36
|
+
}
|
|
37
|
+
error(code, message, details = {}) {
|
|
38
|
+
if (this.json)
|
|
39
|
+
this.stderr.write(`${JSON.stringify({ error: { code, message, ...details } })}\n`);
|
|
40
|
+
else
|
|
41
|
+
this.stderr.write(`error: ${clean(message)}\n`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,iEAAiE;AACjE,yFAAyF;AACzF,MAAM,OAAO,GAAG,4CAA4C,CAAC;AAE7D,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAQzE;iDACiD;AACjD,MAAM,OAAO,MAAM;IACR,IAAI,CAAU;IACN,MAAM,CAAwB;IAC9B,MAAM,CAAwB;IAE/C,YAAY,OAAsB;QAChC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IACjD,CAAC;IAED,yFAAyF;IACzF,MAAM,CAAC,IAAa,EAAE,MAAwC;QAC5D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACxD,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9E,CAAC;IAED,wCAAwC;IACxC,QAAQ,CAAC,OAAe;QACtB,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC;IAED,uGAAuG;IACvG,MAAM,CAAC,OAAe;QACpB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,OAAe,EAAE,UAAmC,EAAE;QACxE,IAAI,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;;YAC7F,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One change a command would make at Narai. */
|
|
2
|
+
export interface Change {
|
|
3
|
+
action: "create" | "update" | "upload";
|
|
4
|
+
target: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const planFlags: {
|
|
8
|
+
plan: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
"detailed-exitcode": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
export declare const describeChange: (c: Change) => string;
|
|
12
|
+
/** The exit a `--plan` ends with: `changes` (2) with --detailed-exitcode when there's something to do. */
|
|
13
|
+
export declare const planExit: (changes: readonly Change[], detailed: boolean) => number;
|
package/dist/lib/plan.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { ExitCode } from "./errors.js";
|
|
3
|
+
export const planFlags = {
|
|
4
|
+
plan: Flags.boolean({ description: "Only show what would change; change nothing." }),
|
|
5
|
+
"detailed-exitcode": Flags.boolean({ description: "With --plan: exit 2 when there are changes, 0 when there are none (1 is an error)." }),
|
|
6
|
+
};
|
|
7
|
+
export const describeChange = (c) => ` ${c.action === "create" ? "+" : c.action === "upload" ? "↑" : "~"} ${c.target} ${c.detail}`;
|
|
8
|
+
/** The exit a `--plan` ends with: `changes` (2) with --detailed-exitcode when there's something to do. */
|
|
9
|
+
export const planExit = (changes, detailed) => detailed && changes.length > 0 ? ExitCode.changes : ExitCode.ok;
|
|
10
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/lib/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvC,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;IACpF,mBAAmB,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,oFAAoF,EAAE,CAAC;CAC1I,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AAErJ,0GAA0G;AAC1G,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAA0B,EAAE,QAAiB,EAAU,EAAE,CAChF,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ToolManifest } from "@michinarai/sdk";
|
|
2
|
+
/** A tool's manifest, at the root of its repository. */
|
|
3
|
+
export declare const MANIFEST = "narai.tool.json";
|
|
4
|
+
/** The tool's root: the nearest directory, from `from` up, that holds a manifest. */
|
|
5
|
+
export declare function toolRoot(from: string): Promise<string>;
|
|
6
|
+
export declare function readManifest(root: string): Promise<unknown>;
|
|
7
|
+
/** The manifest, or every reason it isn't one (the SDK's checks: the schema and the registry's rules). */
|
|
8
|
+
export declare function checkedManifest(root: string): Promise<ToolManifest>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { manifestProblems } from "@michinarai/sdk";
|
|
4
|
+
import { NaraiError } from "./errors.js";
|
|
5
|
+
/** A tool's manifest, at the root of its repository. */
|
|
6
|
+
export const MANIFEST = "narai.tool.json";
|
|
7
|
+
/** The tool's root: the nearest directory, from `from` up, that holds a manifest. */
|
|
8
|
+
export async function toolRoot(from) {
|
|
9
|
+
for (let dir = resolve(from);; dir = dirname(dir)) {
|
|
10
|
+
try {
|
|
11
|
+
await readFile(join(dir, MANIFEST));
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
if (dirname(dir) === dir)
|
|
16
|
+
throw new NaraiError(`no ${MANIFEST} here or above: run narai init, or cd into a tool`, "no_tool");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function readManifest(root) {
|
|
21
|
+
const text = await readFile(join(root, MANIFEST), "utf8");
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new NaraiError(`${MANIFEST} isn't JSON: ${error.message}`, "manifest_invalid");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The manifest, or every reason it isn't one (the SDK's checks: the schema and the registry's rules). */
|
|
30
|
+
export async function checkedManifest(root) {
|
|
31
|
+
const value = await readManifest(root);
|
|
32
|
+
const problems = manifestProblems(value);
|
|
33
|
+
if (problems.length)
|
|
34
|
+
throw new NaraiError(`${MANIFEST} doesn't match the contract`, "manifest_invalid", 1, { problems });
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/lib/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAqB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,wDAAwD;AACxD,MAAM,CAAC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAE1C,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,KAAK,IAAI,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,GAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;YACpC,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG;gBAAE,MAAM,IAAI,UAAU,CAAC,MAAM,QAAQ,mDAAmD,EAAE,SAAS,CAAC,CAAC;QAC/H,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1D,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,UAAU,CAAC,GAAG,QAAQ,gBAAiB,KAAe,CAAC,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAC;IAClG,CAAC;AACH,CAAC;AAED,0GAA0G;AAC1G,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IAChD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,QAAQ,CAAC,MAAM;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,QAAQ,6BAA6B,EAAE,kBAAkB,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IACzH,OAAO,KAAqB,CAAC;AAC/B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michinarai/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The narai command: scaffold, validate, build, run and register tools for Narai lessons.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Monster-Orange/mo-narai-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Monster-Orange/mo-narai-cli#readme",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"narai": "bin/run.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"dist",
|
|
21
|
+
"templates"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"oclif": {
|
|
27
|
+
"bin": "narai",
|
|
28
|
+
"dirname": "narai",
|
|
29
|
+
"commands": {
|
|
30
|
+
"strategy": "explicit",
|
|
31
|
+
"target": "./dist/commands.js",
|
|
32
|
+
"identifier": "COMMANDS"
|
|
33
|
+
},
|
|
34
|
+
"topicSeparator": " "
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
38
|
+
"prepare": "npm run build",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"lint": "eslint . --max-warnings=0",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"generate": "openapi-typescript node_modules/@michinarai/sdk/schema/openapi.tools.json --output src/generated/api.ts"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@inquirer/confirm": "6.3.3",
|
|
46
|
+
"@michinarai/sdk": "0.2.1",
|
|
47
|
+
"@napi-rs/keyring": "2.1.1",
|
|
48
|
+
"@oclif/core": "5.1.7",
|
|
49
|
+
"openapi-fetch": "0.17.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "9.39.5",
|
|
53
|
+
"@types/node": "22.20.5",
|
|
54
|
+
"eslint": "9.39.5",
|
|
55
|
+
"globals": "14.0.0",
|
|
56
|
+
"openapi-typescript": "7.13.0",
|
|
57
|
+
"typescript": "5.9.3",
|
|
58
|
+
"typescript-eslint": "8.72.1",
|
|
59
|
+
"vitest": "3.2.7"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: narai-tool
|
|
3
|
+
description: Build, test and ship this Narai tool. Use when changing what the tool shows, what it reports (answers, practice), the commands it takes, or its manifest; and before pushing a version.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working on a Narai tool
|
|
7
|
+
|
|
8
|
+
1. Change the manifest first (`narai.tool.json`): declare every payload under `payloads` with a JSON Schema, map each
|
|
9
|
+
task kind under `answers`, each activity under `practice`, each command under `commands`. Run `narai validate`.
|
|
10
|
+
2. Change the tool (`src/`). Send only through the SDK session: `tool.answer(payload)` for the task Narai started,
|
|
11
|
+
`tool.practice(activity, { duration_s, attempts, payload })` outside a task, `tool.onCommand(name, fn)` for what the
|
|
12
|
+
Tutor may ask. Never a verdict field.
|
|
13
|
+
3. Run it: `npm run dev`, open the host address. The harness shows every message and refuses what the manifest doesn't
|
|
14
|
+
declare, with the reason. Send the manifest's commands from its panel. `--task task.json` starts it on a task.
|
|
15
|
+
4. Seal and ship: `npm run build` (writes `dist/narai.tool.json` with every file's sha256), then
|
|
16
|
+
`narai tools push --plan` to see what would change, and `narai tools push` to upload the draft.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Building {{name}}, a Narai tool
|
|
2
|
+
|
|
3
|
+
A Narai tool is a small web app that Narai mounts in a lesson, in a sandboxed frame on its own origin. It shows the
|
|
4
|
+
learner an instrument and reports what the learner did. Narai keeps the answer keys, runs every judge and owns the
|
|
5
|
+
learner: a tool never grades and never learns who the learner is.
|
|
6
|
+
|
|
7
|
+
## The rules
|
|
8
|
+
|
|
9
|
+
- `narai.tool.json` is the tool's only source of permissions. A command, an answer, a practice activity or a payload
|
|
10
|
+
the manifest doesn't declare never crosses: the SDK refuses to send it and Narai refuses to take it.
|
|
11
|
+
- Every payload is `{ "schema": "<family>.<name>@<N>", ...body }`, and its body matches the JSON Schema declared under
|
|
12
|
+
`payloads`.
|
|
13
|
+
- No verdict, ever: no `correct`, `score`, `passed`, `verdict`, `grade` (the list is the SDK's `VERDICT_KEYS`) in an
|
|
14
|
+
answer's schema or body. Report what the learner did; Narai decides what it means.
|
|
15
|
+
- With `assist: honors`, a task whose `assist` is `none` gets no hints and no self-checks from the tool.
|
|
16
|
+
- Show the task's `prompt` as Narai sends it; it is already in the learner's language. Say `ready` in the locale the
|
|
17
|
+
tool actually renders.
|
|
18
|
+
- Only the SDK talks to Narai (`connect`, `ready`, `answer`, `practice`, `onCommand`, `close`). No other `postMessage`,
|
|
19
|
+
no network unless `capabilities.network` says so.
|
|
20
|
+
|
|
21
|
+
## The loop
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install
|
|
25
|
+
npm run dev # build, then the local harness: open the host address it prints
|
|
26
|
+
npm run validate # the manifest against the contract
|
|
27
|
+
npm run build # dist/, sealed: every file's sha256 and the bundle digest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`narai tools push` uploads a sealed build as a draft version. See the skill in `.claude/skills/narai-tool/`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "{{id}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"sdk": "^1",
|
|
5
|
+
"publisher": { "name": "{{publisher}}", "url": "https://example.com" },
|
|
6
|
+
"surfaces": { "embed": { "url": "https://example.com/{{name}}/", "sdk": "^1" } },
|
|
7
|
+
"payloads": {
|
|
8
|
+
"{{family}}.answer@1": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": { "text": { "type": "string", "maxLength": 200 } },
|
|
11
|
+
"required": ["text"],
|
|
12
|
+
"additionalProperties": false
|
|
13
|
+
},
|
|
14
|
+
"{{family}}.drill@1": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": { "rounds": { "type": "integer", "minimum": 0 } },
|
|
17
|
+
"required": ["rounds"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"answers": { "{{family}}.say": "{{family}}.answer@1" },
|
|
21
|
+
"practice": { "drill": "{{family}}.drill@1" },
|
|
22
|
+
"assist": "honors",
|
|
23
|
+
"locales": ["en", "ja"],
|
|
24
|
+
"capabilities": { "audio_out": false, "microphone": false, "network": "none" }
|
|
25
|
+
}
|