@coswic/aipay 0.1.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.
Files changed (52) hide show
  1. package/README.md +55 -0
  2. package/dist/agent-guide.d.ts +18 -0
  3. package/dist/agent-guide.js +60 -0
  4. package/dist/args.d.ts +15 -0
  5. package/dist/args.js +47 -0
  6. package/dist/auth-store.d.ts +23 -0
  7. package/dist/auth-store.js +113 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +8 -0
  10. package/dist/browser.d.ts +1 -0
  11. package/dist/browser.js +18 -0
  12. package/dist/commands/apps.d.ts +15 -0
  13. package/dist/commands/apps.js +53 -0
  14. package/dist/commands/doctor.d.ts +17 -0
  15. package/dist/commands/doctor.js +153 -0
  16. package/dist/commands/init.d.ts +18 -0
  17. package/dist/commands/init.js +252 -0
  18. package/dist/commands/login.d.ts +25 -0
  19. package/dist/commands/login.js +149 -0
  20. package/dist/commands/session.d.ts +6 -0
  21. package/dist/commands/session.js +63 -0
  22. package/dist/commands/status.d.ts +3 -0
  23. package/dist/commands/status.js +24 -0
  24. package/dist/commands/test.d.ts +12 -0
  25. package/dist/commands/test.js +159 -0
  26. package/dist/commands/unlink.d.ts +5 -0
  27. package/dist/commands/unlink.js +70 -0
  28. package/dist/detect.d.ts +27 -0
  29. package/dist/detect.js +79 -0
  30. package/dist/developer-api.d.ts +14 -0
  31. package/dist/developer-api.js +74 -0
  32. package/dist/gate.d.ts +13 -0
  33. package/dist/gate.js +29 -0
  34. package/dist/index.d.ts +22 -0
  35. package/dist/index.js +22 -0
  36. package/dist/loopback.d.ts +37 -0
  37. package/dist/loopback.js +95 -0
  38. package/dist/main.d.ts +5 -0
  39. package/dist/main.js +90 -0
  40. package/dist/output.d.ts +36 -0
  41. package/dist/output.js +52 -0
  42. package/dist/project.d.ts +50 -0
  43. package/dist/project.js +107 -0
  44. package/dist/remote.d.ts +21 -0
  45. package/dist/remote.js +41 -0
  46. package/dist/setup-events.d.ts +3 -0
  47. package/dist/setup-events.js +28 -0
  48. package/dist/templates.d.ts +24 -0
  49. package/dist/templates.js +226 -0
  50. package/dist/version.d.ts +1 -0
  51. package/dist/version.js +10 -0
  52. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @coswic/aipay — AI-Pay CLI
2
+
3
+ The command-line companion to [`@coswic/aipay-sdk`](https://www.npmjs.com/package/@coswic/aipay-sdk).
4
+ Made for developers and for the coding agents they hand the Console prompt to.
5
+
6
+ ```bash
7
+ npx @coswic/aipay init --app <client_id> --auth <authorization server> --api <api base>
8
+ npx @coswic/aipay test
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ | Command | What it does |
14
+ |---|---|
15
+ | `aipay init` | Links the project to your app: writes `.aipay/project.json` (commit it), `.env.local` (public values only, git-ignored), a start/callback route for your framework (Next.js, Express, Fastify, Hono, plain Node), `aipay-agent.md` (the developer docs, fetched once, for your agent to read offline) and installs the SDK. Never overwrites a file it did not write this run. `--dry-run` shows the plan. |
16
+ | `aipay test` | The proof: a real sign-in through the AI-Pay authorization server, received by the CLI on a loopback callback, then one chat completion on that user's behalf. Prints the receipt (`request_id`, amount charged). The Console's verify step turns green from the server side. The authorization server ignores the port for `127.0.0.1` redirect URIs (RFC 8252), so the CLI binds any free port and your dev server can keep running; `--port <n>` forces one. `--no-open` prints the URL instead of opening a browser; `--model <id>` picks the model. |
17
+ | `aipay doctor` | Checks Node, the link, `.env.local`, the SDK install, and that the authorization server and API are reachable. `--offline` skips the network. |
18
+ | `aipay status` | What this directory is linked to. |
19
+ | `aipay unlink` | Forgets the link (removes `.aipay/project.json`, strips `AIPAY_*` from `.env.local`); keeps your code. |
20
+
21
+ Every command accepts `--json` (one JSON document on stdout) and `--dir <path>`.
22
+
23
+ Exit codes: `0` ok · `1` a problem you can fix locally · `2` usage · `3` network/auth.
24
+
25
+ ## Pilot gate
26
+
27
+ During the closed pilot, non-public AI-Pay paths answer `404` with an empty body unless the
28
+ request carries the gate token. Export it and the CLI sends it as `X-AIPay-Gate`:
29
+
30
+ ```bash
31
+ export AIPAY_GATE_TOKEN=<token from the Console>
32
+ ```
33
+
34
+ The CLI never writes the token to any file and never prints it.
35
+
36
+ ## What gets written
37
+
38
+ ```
39
+ .aipay/project.json # app client_id, endpoints, callback, what was scaffolded — no secrets
40
+ .env.local # AIPAY_CLIENT_ID / AIPAY_AUTH_ORIGIN / AIPAY_API_BASE / AIPAY_CALLBACK_URL
41
+ aipay-agent.md # project facts + the developer docs, for coding agents
42
+ src/aipay.ts # sign-in helpers (start → callback → first completion)
43
+ src/aipay-routes.ts # framework wiring (or app/auth/aipay/*/route.ts for Next.js)
44
+ ```
45
+
46
+ Access and refresh tokens obtained by `aipay test` live in memory only.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ pnpm --filter @coswic/aipay test
52
+ pnpm --filter @coswic/aipay build
53
+ ```
54
+
55
+ Zero runtime dependencies besides the SDK. Node 20+.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `aipay-agent.md`: the file a coding agent reads first. Header = the facts of THIS
3
+ * project (app, endpoints, what was scaffolded, what "done" means); body = the AI-Pay
4
+ * developer docs fetched from the API (`/docs.md?lang=en`), so the agent does not need
5
+ * network access — or the gate token — to read them later.
6
+ *
7
+ * Never contains tokens. If the docs are gated (404 with empty body) the file still gets
8
+ * written with the header and a pointer, and the CLI warns.
9
+ */
10
+ import type { ProjectConfig } from './project.js';
11
+ export declare const AGENT_GUIDE_FILE = "aipay-agent.md";
12
+ export interface GuideInput {
13
+ project: ProjectConfig;
14
+ scaffolded: string[];
15
+ docsMarkdown: string | null;
16
+ docsUrl: string;
17
+ }
18
+ export declare function agentGuide(input: GuideInput): string;
@@ -0,0 +1,60 @@
1
+ export const AGENT_GUIDE_FILE = 'aipay-agent.md';
2
+ export function agentGuide(input) {
3
+ const p = input.project;
4
+ const files = input.scaffolded.length
5
+ ? input.scaffolded.map((f) => `- \`${f}\``).join('\n')
6
+ : '- (none — the project had them already)';
7
+ const head = `# AI-Pay integration guide for this project
8
+
9
+ > Written by \`aipay init\` on ${p.linked_at}. Regenerate with \`aipay init --app ${p.app.client_id}\`. Safe to commit: it contains no secrets.
10
+
11
+ ## This project
12
+
13
+ | | |
14
+ |---|---|
15
+ | App | ${p.app.name ?? '(unnamed)'} |
16
+ | client_id | \`${p.app.client_id}\` (public client, PKCE S256, no secret) |
17
+ | Authorization server | ${p.auth_origin} |
18
+ | API base | ${p.api_base} |
19
+ | Registered callback | \`${p.callback}\` (for 127.0.0.1 the port is not compared, so any local port works) |
20
+ | Local config | \`.aipay/project.json\` (commit it), \`.env.local\` (do not commit) |
21
+
22
+ Files scaffolded by the CLI (edit freely; the CLI never overwrites existing files):
23
+ ${files}
24
+
25
+ ## Definition of done
26
+
27
+ 1. \`aipay doctor\` reports no problems.
28
+ 2. The app's dev server serves the start route and the callback route above.
29
+ 3. \`aipay test\` completes: a real sign-in through the AI-Pay authorization server and
30
+ one chat completion whose response carries a receipt (\`aipay.request_id\`).
31
+ 4. Typecheck/build pass. Do not commit automatically and do not deploy.
32
+
33
+ ## Rules for agents
34
+
35
+ - Read the docs below before changing the integration; they are the source of truth.
36
+ - The pilot gate token comes only from the \`AIPAY_GATE_TOKEN\` environment variable.
37
+ Never write it into code, config files, or version control.
38
+ - Never request \`offline_access\` unless the product needs background renewal.
39
+ - Key user accounts on \`sub\` from the userinfo endpoint, never on email.
40
+
41
+ `;
42
+ const body = input.docsMarkdown
43
+ ? `---
44
+
45
+ # AI-Pay developer documentation
46
+
47
+ _Fetched from ${input.docsUrl} by \`aipay init\`._
48
+
49
+ ${input.docsMarkdown.trim()}
50
+ `
51
+ : `---
52
+
53
+ # AI-Pay developer documentation
54
+
55
+ The docs could not be fetched when this file was written (see the \`aipay init\` output).
56
+ Read them at ${input.docsUrl} — during the pilot send the header \`X-AIPay-Gate\` with the
57
+ value of \`AIPAY_GATE_TOKEN\`, or run \`aipay init\` again with the variable set.
58
+ `;
59
+ return head + body;
60
+ }
package/dist/args.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tiny argv parser — zero deps on purpose (the CLI is installed by coding agents in
3
+ * arbitrary projects; every dependency is attack surface and install time).
4
+ *
5
+ * Supports `--flag`, `--flag value`, `--flag=value`, `--no-flag`, and positionals.
6
+ * Every flag is a string or boolean; commands coerce what they need.
7
+ */
8
+ export interface ParsedArgs {
9
+ command: string | null;
10
+ positionals: string[];
11
+ flags: Record<string, string | boolean>;
12
+ }
13
+ export declare function parseArgs(argv: string[]): ParsedArgs;
14
+ export declare function flagString(flags: ParsedArgs['flags'], name: string): string | null;
15
+ export declare function flagBool(flags: ParsedArgs['flags'], name: string, dflt?: boolean): boolean;
package/dist/args.js ADDED
@@ -0,0 +1,47 @@
1
+ export function parseArgs(argv) {
2
+ const out = { command: null, positionals: [], flags: {} };
3
+ for (let i = 0; i < argv.length; i++) {
4
+ const a = argv[i] ?? '';
5
+ if (a === '--') {
6
+ out.positionals.push(...argv.slice(i + 1));
7
+ break;
8
+ }
9
+ if (a.startsWith('--')) {
10
+ const body = a.slice(2);
11
+ const eq = body.indexOf('=');
12
+ if (eq >= 0) {
13
+ out.flags[body.slice(0, eq)] = body.slice(eq + 1);
14
+ continue;
15
+ }
16
+ if (body.startsWith('no-')) {
17
+ out.flags[body.slice(3)] = false;
18
+ continue;
19
+ }
20
+ const next = argv[i + 1];
21
+ if (next !== undefined && !next.startsWith('--')) {
22
+ out.flags[body] = next;
23
+ i++;
24
+ }
25
+ else
26
+ out.flags[body] = true;
27
+ continue;
28
+ }
29
+ if (out.command === null)
30
+ out.command = a;
31
+ else
32
+ out.positionals.push(a);
33
+ }
34
+ return out;
35
+ }
36
+ export function flagString(flags, name) {
37
+ const v = flags[name];
38
+ return typeof v === 'string' && v !== '' ? v : null;
39
+ }
40
+ export function flagBool(flags, name, dflt = false) {
41
+ const v = flags[name];
42
+ if (typeof v === 'boolean')
43
+ return v;
44
+ if (typeof v === 'string')
45
+ return v !== 'false' && v !== '0';
46
+ return dflt;
47
+ }
@@ -0,0 +1,23 @@
1
+ export interface StoredAuth {
2
+ api_base: string;
3
+ auth_origin: string;
4
+ client_id: string;
5
+ access_token: string;
6
+ refresh_token: string | null;
7
+ /** epoch ms */
8
+ expires_at: number;
9
+ scope: string;
10
+ email: string | null;
11
+ sub: string;
12
+ }
13
+ export interface AuthStore {
14
+ kind: 'keychain' | 'file';
15
+ load(): StoredAuth | null;
16
+ save(auth: StoredAuth): void;
17
+ clear(): void;
18
+ /** Human description of where tokens are kept (for `whoami`/`login` output). */
19
+ where: string;
20
+ }
21
+ export declare function configDir(env?: NodeJS.ProcessEnv): string;
22
+ export declare function createAuthStore(env?: NodeJS.ProcessEnv): AuthStore;
23
+ export declare function fileStore(dir: string): AuthStore;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Where the developer's management tokens live between commands.
3
+ *
4
+ * Order of preference (decision D5, 2026-09-04): the OS keychain when a CLI for it exists
5
+ * (macOS `security`, Linux `secret-tool`), else a 0600 file under the user's config dir
6
+ * with a one-line warning at login. Tokens are never written into the project directory.
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ const SERVICE = 'aipay-cli';
13
+ export function configDir(env = process.env) {
14
+ if (env.AIPAY_CONFIG_DIR)
15
+ return env.AIPAY_CONFIG_DIR;
16
+ const base = process.platform === 'win32'
17
+ ? (env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'))
18
+ : (env.XDG_CONFIG_HOME ?? join(homedir(), '.config'));
19
+ return join(base, 'aipay');
20
+ }
21
+ function has(bin) {
22
+ const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { stdio: 'ignore' });
23
+ return r.status === 0;
24
+ }
25
+ export function createAuthStore(env = process.env) {
26
+ if (env.AIPAY_TOKEN_STORE !== 'file') {
27
+ if (process.platform === 'darwin' && has('security'))
28
+ return macKeychain();
29
+ if (process.platform === 'linux' && has('secret-tool'))
30
+ return secretTool();
31
+ }
32
+ return fileStore(configDir(env));
33
+ }
34
+ function macKeychain() {
35
+ const account = 'developer';
36
+ return {
37
+ kind: 'keychain',
38
+ where: 'macOS Keychain (service aipay-cli)',
39
+ load() {
40
+ const r = spawnSync('security', ['find-generic-password', '-a', account, '-s', SERVICE, '-w'], { encoding: 'utf8' });
41
+ if (r.status !== 0)
42
+ return null;
43
+ return parse(r.stdout.trim());
44
+ },
45
+ save(auth) {
46
+ const r = spawnSync('security', ['add-generic-password', '-U', '-a', account, '-s', SERVICE, '-w', JSON.stringify(auth)], { stdio: 'ignore' });
47
+ if (r.status !== 0)
48
+ throw new Error('could not write to the macOS Keychain');
49
+ },
50
+ clear() {
51
+ spawnSync('security', ['delete-generic-password', '-a', account, '-s', SERVICE], {
52
+ stdio: 'ignore',
53
+ });
54
+ },
55
+ };
56
+ }
57
+ function secretTool() {
58
+ const attrs = ['service', SERVICE, 'account', 'developer'];
59
+ return {
60
+ kind: 'keychain',
61
+ where: 'the system keyring (secret-tool, service aipay-cli)',
62
+ load() {
63
+ const r = spawnSync('secret-tool', ['lookup', ...attrs], { encoding: 'utf8' });
64
+ if (r.status !== 0 || !r.stdout)
65
+ return null;
66
+ return parse(r.stdout.trim());
67
+ },
68
+ save(auth) {
69
+ const r = spawnSync('secret-tool', ['store', '--label', 'AI-Pay CLI', ...attrs], {
70
+ input: JSON.stringify(auth),
71
+ stdio: ['pipe', 'ignore', 'ignore'],
72
+ });
73
+ if (r.status !== 0)
74
+ throw new Error('could not write to the system keyring');
75
+ },
76
+ clear() {
77
+ spawnSync('secret-tool', ['clear', ...attrs], { stdio: 'ignore' });
78
+ },
79
+ };
80
+ }
81
+ export function fileStore(dir) {
82
+ const file = join(dir, 'credentials.json');
83
+ return {
84
+ kind: 'file',
85
+ where: `${file} (mode 0600)`,
86
+ load() {
87
+ if (!existsSync(file))
88
+ return null;
89
+ return parse(readFileSync(file, 'utf8'));
90
+ },
91
+ save(auth) {
92
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
93
+ writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
94
+ chmodSync(file, 0o600);
95
+ },
96
+ clear() {
97
+ rmSync(file, { force: true });
98
+ },
99
+ };
100
+ }
101
+ function parse(raw) {
102
+ try {
103
+ const v = JSON.parse(raw);
104
+ if (typeof v.access_token !== 'string' ||
105
+ typeof v.api_base !== 'string' ||
106
+ typeof v.client_id !== 'string')
107
+ return null;
108
+ return v;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './main.js';
3
+ main(process.argv.slice(2)).then((code) => {
4
+ process.exitCode = code;
5
+ }, (err) => {
6
+ process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1 @@
1
+ export declare function openBrowser(url: string, platform?: NodeJS.Platform): boolean;
@@ -0,0 +1,18 @@
1
+ /** Open a URL in the user's default browser without depending on the `open` package. */
2
+ import { spawn } from 'node:child_process';
3
+ export function openBrowser(url, platform = process.platform) {
4
+ const [cmd, args] = platform === 'darwin'
5
+ ? ['open', [url]]
6
+ : platform === 'win32'
7
+ ? ['cmd', ['/c', 'start', '', url.replace(/&/g, '^&')]]
8
+ : ['xdg-open', [url]];
9
+ try {
10
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
11
+ child.on('error', () => { });
12
+ child.unref();
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ import { type ParsedArgs } from '../args.js';
2
+ import { type DeveloperApi } from '../developer-api.js';
3
+ import { type Reporter } from '../output.js';
4
+ export interface CreatedApp {
5
+ app_id: string;
6
+ client_id: string;
7
+ redirect_uris: string[];
8
+ replayed?: boolean;
9
+ }
10
+ export declare function createApp(api: DeveloperApi, input: {
11
+ name: string;
12
+ callback: string;
13
+ key?: string | null;
14
+ }): Promise<CreatedApp>;
15
+ export declare function runApps(args: ParsedArgs, out: Reporter, env?: NodeJS.ProcessEnv): Promise<number>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `aipay apps list [--json]`
3
+ * `aipay apps create --name <name> [--callback <uri>] [--key <idempotency key>] [--json]`
4
+ *
5
+ * Uses the developer's management token. Creation sends an Idempotency-Key so a retried
6
+ * command never makes a second app.
7
+ */
8
+ import { randomUUID } from 'node:crypto';
9
+ import { flagString } from '../args.js';
10
+ import { createAuthStore } from '../auth-store.js';
11
+ import { errorMessage, openDeveloperApi } from '../developer-api.js';
12
+ import { CliError, EXIT } from '../output.js';
13
+ import { DEFAULT_CALLBACK } from './init.js';
14
+ export async function createApp(api, input) {
15
+ const key = input.key ?? `aipay-cli-${randomUUID()}`;
16
+ const res = await api.request('POST', '/v1/developer/apps', {
17
+ body: { name: input.name, redirect_uris: [input.callback] },
18
+ headers: { 'idempotency-key': key },
19
+ });
20
+ if (res.status !== 201 && res.status !== 200)
21
+ throw new CliError('create_failed', `could not create the app: ${errorMessage(res.body, res.status)}`, EXIT.remote);
22
+ return res.body;
23
+ }
24
+ export async function runApps(args, out, env = process.env) {
25
+ const sub = args.positionals[0] ?? 'list';
26
+ const store = createAuthStore(env);
27
+ const api = await openDeveloperApi(store, env);
28
+ if (sub === 'list') {
29
+ const res = await api.request('GET', '/v1/developer/apps');
30
+ if (res.status !== 200)
31
+ throw new CliError('remote', `GET /v1/developer/apps → ${errorMessage(res.body, res.status)}`, EXIT.remote);
32
+ const apps = Array.isArray(res.body) ? res.body : (res.body.data ?? res.body.apps ?? []);
33
+ if (!apps.length)
34
+ out.line('info', 'no apps yet — `aipay apps create --name "My app"`');
35
+ for (const a of apps)
36
+ out.line('info', `${String(a.name ?? '?')} ${String(a.id ?? a.app_id ?? a.client_id ?? '')} ${String(a.status ?? '')}`);
37
+ out.result({ ok: true, apps });
38
+ return EXIT.ok;
39
+ }
40
+ if (sub === 'create') {
41
+ const name = flagString(args.flags, 'name');
42
+ if (!name)
43
+ throw new CliError('usage', 'missing --name <app name>', EXIT.usage);
44
+ const callback = flagString(args.flags, 'callback') ?? DEFAULT_CALLBACK;
45
+ const created = await createApp(api, { name, callback, key: flagString(args.flags, 'key') });
46
+ out.line('ok', `${created.replayed ? 'already created' : 'created'} "${name}" — client_id ${created.client_id}`);
47
+ out.kv('callback', created.redirect_uris.join(', '));
48
+ out.line('info', `next: aipay init --app ${created.client_id}`);
49
+ out.result({ ok: true, ...created, name });
50
+ return EXIT.ok;
51
+ }
52
+ throw new CliError('usage', `unknown apps subcommand "${sub}" (list | create)`, EXIT.usage);
53
+ }
@@ -0,0 +1,17 @@
1
+ import { type ParsedArgs } from '../args.js';
2
+ import { type Level, type Reporter } from '../output.js';
3
+ import { type Remote } from '../remote.js';
4
+ export interface Check {
5
+ id: string;
6
+ level: Level;
7
+ text: string;
8
+ }
9
+ export interface DoctorDeps {
10
+ remote?: Remote;
11
+ env?: NodeJS.ProcessEnv;
12
+ nodeVersion?: string;
13
+ }
14
+ export declare function runDoctor(args: ParsedArgs, out: Reporter, deps?: DoctorDeps): Promise<number>;
15
+ export declare function collectChecks(dir: string, opts: {
16
+ offline: boolean;
17
+ }, deps: DoctorDeps, env: NodeJS.ProcessEnv): Promise<Check[]>;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `aipay doctor [--dir] [--json] [--offline]` — checks the link, the local files, the
3
+ * SDK install, and (unless --offline) reachability of the authorization server and the
4
+ * API. Exit 0 = nothing to fix, 1 = at least one problem.
5
+ */
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join, resolve } from 'node:path';
8
+ import { flagBool, flagString } from '../args.js';
9
+ import { detectProject } from '../detect.js';
10
+ import { GATE_ENV } from '../gate.js';
11
+ import { parseCallback } from '../loopback.js';
12
+ import { EXIT } from '../output.js';
13
+ import { ENV_FILE, ENV_KEYS, readProject } from '../project.js';
14
+ import { createRemote, docsUrl, GATE_HINT } from '../remote.js';
15
+ export async function runDoctor(args, out, deps = {}) {
16
+ const dir = resolve(flagString(args.flags, 'dir') ?? '.');
17
+ const env = deps.env ?? process.env;
18
+ const checks = await collectChecks(dir, { offline: flagBool(args.flags, 'offline') }, deps, env);
19
+ for (const c of checks)
20
+ out.line(c.level, c.text);
21
+ const problems = checks.filter((c) => c.level === 'bad');
22
+ out.blank();
23
+ out.line(problems.length ? 'bad' : 'ok', problems.length ? `${problems.length} problem(s)` : 'all good');
24
+ out.result({ ok: problems.length === 0, dir, checks });
25
+ return problems.length ? EXIT.problem : EXIT.ok;
26
+ }
27
+ export async function collectChecks(dir, opts, deps, env) {
28
+ const checks = [];
29
+ const major = Number((deps.nodeVersion ?? process.versions.node).split('.')[0]);
30
+ checks.push(major >= 20
31
+ ? { id: 'node', level: 'ok', text: `Node ${deps.nodeVersion ?? process.versions.node}` }
32
+ : {
33
+ id: 'node',
34
+ level: 'bad',
35
+ text: `Node ${deps.nodeVersion ?? process.versions.node} — the SDK needs 20+`,
36
+ });
37
+ let project = null;
38
+ try {
39
+ project = readProject(dir);
40
+ }
41
+ catch (e) {
42
+ checks.push({ id: 'project', level: 'bad', text: e.message });
43
+ return checks;
44
+ }
45
+ if (!project) {
46
+ checks.push({
47
+ id: 'project',
48
+ level: 'bad',
49
+ text: 'not linked — run `aipay init --app <client_id> --auth <origin> --api <base>`',
50
+ });
51
+ return checks;
52
+ }
53
+ checks.push({
54
+ id: 'project',
55
+ level: 'ok',
56
+ text: `linked to ${project.app.name ?? project.app.client_id} (.aipay/project.json)`,
57
+ });
58
+ try {
59
+ const t = parseCallback(project.callback);
60
+ checks.push({
61
+ id: 'callback',
62
+ level: 'ok',
63
+ text: `callback ${project.callback} (loopback, port ${t.port})`,
64
+ });
65
+ }
66
+ catch (e) {
67
+ checks.push({ id: 'callback', level: 'bad', text: `callback: ${e.message}` });
68
+ }
69
+ const envPath = join(dir, ENV_FILE);
70
+ if (!existsSync(envPath))
71
+ checks.push({ id: 'env', level: 'bad', text: `${ENV_FILE} missing — rerun \`aipay init\`` });
72
+ else {
73
+ const content = readFileSync(envPath, 'utf8');
74
+ const want = {
75
+ [ENV_KEYS.clientId]: project.app.client_id,
76
+ [ENV_KEYS.authOrigin]: project.auth_origin,
77
+ [ENV_KEYS.apiBase]: project.api_base,
78
+ [ENV_KEYS.callback]: project.callback,
79
+ };
80
+ const wrong = Object.entries(want)
81
+ .filter(([k, v]) => !new RegExp(`^\\s*(?:export\\s+)?${k}\\s*=\\s*["']?${escapeRe(v)}["']?\\s*$`, 'm').test(content))
82
+ .map(([k]) => k);
83
+ checks.push(wrong.length
84
+ ? {
85
+ id: 'env',
86
+ level: 'bad',
87
+ text: `${ENV_FILE} out of sync: ${wrong.join(', ')} — rerun \`aipay init\``,
88
+ }
89
+ : { id: 'env', level: 'ok', text: `${ENV_FILE} has the four AIPAY_* values` });
90
+ }
91
+ const gi = join(dir, '.gitignore');
92
+ if (existsSync(gi) &&
93
+ !readFileSync(gi, 'utf8')
94
+ .split(/\r?\n/)
95
+ .some((l) => l.trim() === ENV_FILE))
96
+ checks.push({ id: 'gitignore', level: 'warn', text: `.gitignore does not list ${ENV_FILE}` });
97
+ const det = detectProject(dir);
98
+ if (det.hasPackageJson) {
99
+ const installed = det.sdkInstalled && existsSync(join(dir, 'node_modules', '@coswic', 'aipay-sdk'));
100
+ checks.push(installed
101
+ ? { id: 'sdk', level: 'ok', text: '@coswic/aipay-sdk installed' }
102
+ : det.sdkInstalled
103
+ ? {
104
+ id: 'sdk',
105
+ level: 'bad',
106
+ text: '@coswic/aipay-sdk is in package.json but not installed — run your package manager install',
107
+ }
108
+ : {
109
+ id: 'sdk',
110
+ level: 'bad',
111
+ text: '@coswic/aipay-sdk not a dependency — rerun `aipay init` (or add it yourself)',
112
+ });
113
+ for (const f of project.scaffold.files)
114
+ if (!existsSync(join(dir, f)))
115
+ checks.push({ id: 'scaffold', level: 'warn', text: `scaffolded file removed: ${f}` });
116
+ }
117
+ if (!env[GATE_ENV])
118
+ checks.push({
119
+ id: 'gate',
120
+ level: 'info',
121
+ text: `${GATE_ENV} not set (fine after the pilot; required during it)`,
122
+ });
123
+ if (!opts.offline) {
124
+ const remote = deps.remote ?? createRemote(env);
125
+ const oidc = await remote.probe(new URL('/.well-known/openid-configuration', project.auth_origin).toString());
126
+ checks.push(oidc.ok
127
+ ? {
128
+ id: 'auth',
129
+ level: 'ok',
130
+ text: `authorization server reachable (${project.auth_origin})`,
131
+ }
132
+ : {
133
+ id: 'auth',
134
+ level: 'bad',
135
+ text: `authorization server ${project.auth_origin}: ${oidc.error ?? `HTTP ${oidc.status}`}`,
136
+ });
137
+ const api = await remote.probe(docsUrl(project.api_base, 'en'));
138
+ if (api.ok)
139
+ checks.push({ id: 'api', level: 'ok', text: `API reachable (${project.api_base})` });
140
+ else if (api.gated)
141
+ checks.push({ id: 'api', level: 'bad', text: `API ${project.api_base}: ${GATE_HINT}` });
142
+ else
143
+ checks.push({
144
+ id: 'api',
145
+ level: 'bad',
146
+ text: `API ${project.api_base}: ${api.error ?? `HTTP ${api.status}`}`,
147
+ });
148
+ }
149
+ return checks;
150
+ }
151
+ function escapeRe(s) {
152
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
153
+ }
@@ -0,0 +1,18 @@
1
+ import { type ParsedArgs } from '../args.js';
2
+ import { type StoredAuth } from '../auth-store.js';
3
+ import { type DeveloperApi } from '../developer-api.js';
4
+ import { type Reporter } from '../output.js';
5
+ import { type Remote } from '../remote.js';
6
+ export declare const DEFAULT_CALLBACK = "http://127.0.0.1:3000/auth/aipay/callback";
7
+ export declare const SDK_PACKAGE = "@coswic/aipay-sdk";
8
+ export interface InitDeps {
9
+ remote?: Remote;
10
+ /** Injected for tests: the developer login (null = signed out). */
11
+ login?: StoredAuth | null;
12
+ developerApi?: DeveloperApi;
13
+ now?: () => Date;
14
+ /** Runs the package-manager install; returns exit code. Injected for tests. */
15
+ run?: (cmd: string[], cwd: string, quiet: boolean) => number;
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export declare function runInit(args: ParsedArgs, out: Reporter, deps?: InitDeps): Promise<number>;