@coswic/aipay 0.2.0 → 0.4.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 (55) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +6 -10
  3. package/dist/agent-guide.d.ts +18 -0
  4. package/dist/agent-guide.js +60 -0
  5. package/dist/args.d.ts +15 -0
  6. package/dist/args.js +47 -0
  7. package/dist/auth-store.d.ts +47 -0
  8. package/dist/auth-store.js +176 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +8 -0
  11. package/dist/browser.d.ts +1 -0
  12. package/dist/browser.js +18 -0
  13. package/dist/commands/apps.d.ts +15 -0
  14. package/dist/commands/apps.js +53 -0
  15. package/dist/commands/doctor.d.ts +16 -0
  16. package/dist/commands/doctor.js +145 -0
  17. package/dist/commands/init.d.ts +18 -0
  18. package/dist/commands/init.js +265 -0
  19. package/dist/commands/login.d.ts +34 -0
  20. package/dist/commands/login.js +187 -0
  21. package/dist/commands/session.d.ts +6 -0
  22. package/dist/commands/session.js +62 -0
  23. package/dist/commands/status.d.ts +3 -0
  24. package/dist/commands/status.js +24 -0
  25. package/dist/commands/test.d.ts +12 -0
  26. package/dist/commands/test.js +159 -0
  27. package/dist/commands/unlink.d.ts +5 -0
  28. package/dist/commands/unlink.js +73 -0
  29. package/dist/detect.d.ts +27 -0
  30. package/dist/detect.js +83 -0
  31. package/dist/developer-api.d.ts +14 -0
  32. package/dist/developer-api.js +73 -0
  33. package/dist/device.d.ts +38 -0
  34. package/dist/device.js +122 -0
  35. package/dist/index.d.ts +22 -0
  36. package/dist/index.js +22 -0
  37. package/dist/loopback.d.ts +37 -0
  38. package/dist/loopback.js +95 -0
  39. package/dist/main.d.ts +5 -0
  40. package/dist/main.js +90 -0
  41. package/dist/network.d.ts +3 -0
  42. package/dist/network.js +5 -0
  43. package/dist/output.d.ts +36 -0
  44. package/dist/output.js +52 -0
  45. package/dist/project.d.ts +61 -0
  46. package/dist/project.js +126 -0
  47. package/dist/remote.d.ts +21 -0
  48. package/dist/remote.js +41 -0
  49. package/dist/setup-events.d.ts +3 -0
  50. package/dist/setup-events.js +28 -0
  51. package/dist/templates.d.ts +24 -0
  52. package/dist/templates.js +333 -0
  53. package/dist/version.d.ts +1 -0
  54. package/dist/version.js +10 -0
  55. package/package.json +4 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # @coswic/aipay
2
+
3
+ ## 0.4.0 — 2026-09-08
4
+
5
+ - Simplify setup instructions around standard OAuth and HTTPS connectivity.
6
+ - Report empty 404 responses without inferring their cause.
7
+ - Remove the retired `AIPAY_GATE_TOKEN` setting and automatic `X-AIPay-Gate` header injection. OAuth tokens, PKCE, device authorization, refresh and revocation retain their existing behavior.
8
+ - **Programmatic compatibility:** remove the `GATE_ENV`, `gatedFetch`, `gateHeaders` and `looksGated` exports; use `isEmpty404` to identify an empty 404 response. Remove the gate-only environment arguments from `createRemote`, `fetchCliConfig`, `openDeveloperApi` and the doctor dependency interface. `Remote.fetchDocs`/`probe` results and init JSON use `empty404` instead of `gated`; the corresponding error code is `empty_404`.
9
+
10
+ ## 0.3.0
11
+
12
+ - `aipay init --framework next|express|fastify|hono|vite|node` overrides framework detection.
13
+ - Vite (browser SPA) projects: `init` writes a browser helper (`aipay.ts`, PKCE in the page,
14
+ tokens in sessionStorage) plus `aipay-boot.ts`, and `VITE_AIPAY_*` aliases in `.env.local`
15
+ (Vite exposes only VITE_-prefixed values). Server frameworks still win when both are present.
16
+ - `aipay test` reports failures to the Console too (`cli.test` with `ok:false` and an error
17
+ code), so the verify step can show what went wrong.
18
+
19
+ ## 0.2.1
20
+
21
+ Same content as 0.2.0. Version 0.2.0 on npm was published by mistake without `dist` (the
22
+ tarball only had README and package.json); it is deprecated and `latest` points past it.
23
+ Install 0.2.1 or later.
24
+
25
+ ## 0.2.0
26
+
27
+ - `aipay login --device`: RFC 8628 device authorization for machines without a browser (SSH
28
+ boxes, containers, CI). Prints a URL and a short code; approve on any device; the CLI polls.
29
+ Requires an AI-Pay whose `/v1/cli/config` reports `device_authorization: true`.
30
+ - Windows: developer tokens are now protected with DPAPI (Windows PowerShell
31
+ `ConvertFrom-SecureString`) in `%APPDATA%\aipay\credentials.dpapi` instead of a plaintext
32
+ `credentials.json`. An existing plaintext file is read once and replaced on the next login.
33
+ `AIPAY_TOKEN_STORE=file` keeps the old behaviour.
34
+ - `aipay login` (browser flow) mentions `--device` when the server supports it.
35
+
36
+ ## 0.1.0
37
+
38
+ - First release: `init`, `test`, `doctor`, `status`, `unlink`, `login`/`logout`/`whoami`,
39
+ `apps list`/`apps create`; loopback PKCE on any free port; tokens in the OS keychain or a
40
+ 0600 file; setup progress reported to the Console.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npx @coswic/aipay test
12
12
 
13
13
  | Command | What it does |
14
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. |
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) or a browser helper for a Vite SPA (`--framework` overrides detection), `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
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
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
18
  | `aipay status` | What this directory is linked to. |
@@ -37,16 +37,12 @@ own. Nothing is written to the project; tokens go to the keychain or the 0600 fi
37
37
  like the browser flow. With `--json`, the URL and code are echoed on stderr as
38
38
  `device_url …` / `user_code …` so an agent can show them to you.
39
39
 
40
- ## Pilot gate
40
+ ## Connection troubleshooting
41
41
 
42
- During the closed pilot, non-public AI-Pay paths answer `404` with an empty body unless the
43
- request carries the gate token. Export it and the CLI sends it as `X-AIPay-Gate`:
44
-
45
- ```bash
46
- export AIPAY_GATE_TOKEN=<token from the Console>
47
- ```
48
-
49
- The CLI never writes the token to any file and never prints it.
42
+ Check the configured API and authorization server URLs and network connectivity if a
43
+ request fails. An empty `404` alone does not identify the cause; it does not establish
44
+ whether a path is missing or why the server returned that response. Run `aipay doctor`
45
+ to check the configured endpoints before retrying.
50
46
 
51
47
  ## What gets written
52
48
 
@@ -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 to read them later.
6
+ *
7
+ * Never contains tokens. If the docs cannot be fetched, 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
+ - Use standard OAuth sign-in and app authorization.
37
+ - Keep OAuth tokens out of code, config files, and 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
+ Check the endpoint URL and network connectivity, then read
57
+ ${input.docsUrl} or run \`aipay init\` again. An empty 404 alone does not identify the cause.
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,47 @@
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`), on Windows a DPAPI-protected file (Windows
6
+ * PowerShell's ConvertFrom-SecureString — encrypted for this Windows user, no plaintext on
7
+ * disk), else a 0600 file under the user's config dir with a one-line warning at login.
8
+ * Tokens are never written into the project directory. `AIPAY_TOKEN_STORE=file` forces the
9
+ * plain file everywhere (escape hatch when a keychain misbehaves).
10
+ */
11
+ import { type SpawnSyncOptions, type SpawnSyncReturns } from 'node:child_process';
12
+ export interface StoredAuth {
13
+ api_base: string;
14
+ auth_origin: string;
15
+ client_id: string;
16
+ access_token: string;
17
+ refresh_token: string | null;
18
+ /** epoch ms */
19
+ expires_at: number;
20
+ scope: string;
21
+ email: string | null;
22
+ sub: string;
23
+ }
24
+ export interface AuthStore {
25
+ /** keychain = OS secret service; dpapi = Windows user-scoped encryption; file = plain 0600 file */
26
+ kind: 'keychain' | 'dpapi' | 'file';
27
+ load(): StoredAuth | null;
28
+ save(auth: StoredAuth): void;
29
+ clear(): void;
30
+ /** Human description of where tokens are kept (for `whoami`/`login` output). */
31
+ where: string;
32
+ }
33
+ export declare function configDir(env?: NodeJS.ProcessEnv): string;
34
+ export declare function createAuthStore(env?: NodeJS.ProcessEnv): AuthStore;
35
+ /** Minimal spawn shape the DPAPI store needs (injectable so the round-trip is unit-testable off Windows). */
36
+ export type Exec = (cmd: string, args: string[], opts: SpawnSyncOptions & {
37
+ encoding: 'utf8';
38
+ }) => Pick<SpawnSyncReturns<string>, 'status' | 'stdout'>;
39
+ /**
40
+ * Windows: DPAPI (user scope) via PowerShell — the same primitive the OS uses for saved
41
+ * credentials. The file holds only the encrypted blob; another Windows account (or a copy
42
+ * of the file on another machine) cannot decrypt it. The token JSON is base64-encoded
43
+ * before protection so no PowerShell console encoding can mangle it. A plain
44
+ * `credentials.json` left by CLI 0.1.0 is still read, and replaced on the next save.
45
+ */
46
+ export declare function dpapiStore(dir: string, exec?: Exec): AuthStore;
47
+ export declare function fileStore(dir: string): AuthStore;
@@ -0,0 +1,176 @@
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`), on Windows a DPAPI-protected file (Windows
6
+ * PowerShell's ConvertFrom-SecureString — encrypted for this Windows user, no plaintext on
7
+ * disk), else a 0600 file under the user's config dir with a one-line warning at login.
8
+ * Tokens are never written into the project directory. `AIPAY_TOKEN_STORE=file` forces the
9
+ * plain file everywhere (escape hatch when a keychain misbehaves).
10
+ */
11
+ import { spawnSync } from 'node:child_process';
12
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ const SERVICE = 'aipay-cli';
16
+ export function configDir(env = process.env) {
17
+ if (env.AIPAY_CONFIG_DIR)
18
+ return env.AIPAY_CONFIG_DIR;
19
+ const base = process.platform === 'win32'
20
+ ? (env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'))
21
+ : (env.XDG_CONFIG_HOME ?? join(homedir(), '.config'));
22
+ return join(base, 'aipay');
23
+ }
24
+ function has(bin) {
25
+ const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { stdio: 'ignore' });
26
+ return r.status === 0;
27
+ }
28
+ export function createAuthStore(env = process.env) {
29
+ if (env.AIPAY_TOKEN_STORE !== 'file') {
30
+ if (process.platform === 'darwin' && has('security'))
31
+ return macKeychain();
32
+ if (process.platform === 'linux' && has('secret-tool'))
33
+ return secretTool();
34
+ if (process.platform === 'win32' && has('powershell'))
35
+ return dpapiStore(configDir(env));
36
+ }
37
+ return fileStore(configDir(env));
38
+ }
39
+ /** Windows PowerShell 5.1 ships with every Windows; `-NoProfile -NonInteractive` keeps it quiet and fast. */
40
+ const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command'];
41
+ /** stdin → DPAPI blob (hex). The secret travels over stdin only — never in argv (visible in Task Manager/ETW). */
42
+ const PS_PROTECT = '$in=[Console]::In.ReadToEnd().Trim(); $s=ConvertTo-SecureString -String $in -AsPlainText -Force; ConvertFrom-SecureString -SecureString $s';
43
+ /** DPAPI blob (hex) on stdin → plaintext on stdout. */
44
+ const PS_UNPROTECT = '$in=[Console]::In.ReadToEnd().Trim(); $s=ConvertTo-SecureString -String $in; [System.Net.NetworkCredential]::new("", $s).Password';
45
+ /**
46
+ * Windows: DPAPI (user scope) via PowerShell — the same primitive the OS uses for saved
47
+ * credentials. The file holds only the encrypted blob; another Windows account (or a copy
48
+ * of the file on another machine) cannot decrypt it. The token JSON is base64-encoded
49
+ * before protection so no PowerShell console encoding can mangle it. A plain
50
+ * `credentials.json` left by CLI 0.1.0 is still read, and replaced on the next save.
51
+ */
52
+ export function dpapiStore(dir, exec = spawnSync) {
53
+ const file = join(dir, 'credentials.dpapi');
54
+ const legacy = fileStore(dir);
55
+ const run = (script, input) => {
56
+ const r = exec('powershell', [...PS_ARGS, script], {
57
+ input,
58
+ encoding: 'utf8',
59
+ stdio: ['pipe', 'pipe', 'ignore'],
60
+ windowsHide: true,
61
+ });
62
+ if (r.status !== 0 || typeof r.stdout !== 'string')
63
+ return null;
64
+ const out = r.stdout.trim();
65
+ return out ? out : null;
66
+ };
67
+ return {
68
+ kind: 'dpapi',
69
+ where: `${file} (encrypted for this Windows user with DPAPI)`,
70
+ load() {
71
+ if (!existsSync(file))
72
+ return legacy.load();
73
+ const plain = run(PS_UNPROTECT, readFileSync(file, 'utf8'));
74
+ if (!plain)
75
+ return null;
76
+ try {
77
+ return parse(Buffer.from(plain, 'base64').toString('utf8'));
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ },
83
+ save(auth) {
84
+ const blob = run(PS_PROTECT, Buffer.from(JSON.stringify(auth), 'utf8').toString('base64'));
85
+ if (!blob)
86
+ throw new Error('could not protect the tokens with Windows DPAPI (PowerShell failed)');
87
+ mkdirSync(dir, { recursive: true });
88
+ writeFileSync(file, `${blob}\n`);
89
+ legacy.clear(); // a 0.1.0 plaintext file must not outlive the encrypted one
90
+ },
91
+ clear() {
92
+ rmSync(file, { force: true });
93
+ legacy.clear();
94
+ },
95
+ };
96
+ }
97
+ function macKeychain() {
98
+ const account = 'developer';
99
+ return {
100
+ kind: 'keychain',
101
+ where: 'macOS Keychain (service aipay-cli)',
102
+ load() {
103
+ const r = spawnSync('security', ['find-generic-password', '-a', account, '-s', SERVICE, '-w'], { encoding: 'utf8' });
104
+ if (r.status !== 0)
105
+ return null;
106
+ return parse(r.stdout.trim());
107
+ },
108
+ save(auth) {
109
+ const r = spawnSync('security', ['add-generic-password', '-U', '-a', account, '-s', SERVICE, '-w', JSON.stringify(auth)], { stdio: 'ignore' });
110
+ if (r.status !== 0)
111
+ throw new Error('could not write to the macOS Keychain');
112
+ },
113
+ clear() {
114
+ spawnSync('security', ['delete-generic-password', '-a', account, '-s', SERVICE], {
115
+ stdio: 'ignore',
116
+ });
117
+ },
118
+ };
119
+ }
120
+ function secretTool() {
121
+ const attrs = ['service', SERVICE, 'account', 'developer'];
122
+ return {
123
+ kind: 'keychain',
124
+ where: 'the system keyring (secret-tool, service aipay-cli)',
125
+ load() {
126
+ const r = spawnSync('secret-tool', ['lookup', ...attrs], { encoding: 'utf8' });
127
+ if (r.status !== 0 || !r.stdout)
128
+ return null;
129
+ return parse(r.stdout.trim());
130
+ },
131
+ save(auth) {
132
+ const r = spawnSync('secret-tool', ['store', '--label', 'AI-Pay CLI', ...attrs], {
133
+ input: JSON.stringify(auth),
134
+ stdio: ['pipe', 'ignore', 'ignore'],
135
+ });
136
+ if (r.status !== 0)
137
+ throw new Error('could not write to the system keyring');
138
+ },
139
+ clear() {
140
+ spawnSync('secret-tool', ['clear', ...attrs], { stdio: 'ignore' });
141
+ },
142
+ };
143
+ }
144
+ export function fileStore(dir) {
145
+ const file = join(dir, 'credentials.json');
146
+ return {
147
+ kind: 'file',
148
+ where: `${file} (mode 0600)`,
149
+ load() {
150
+ if (!existsSync(file))
151
+ return null;
152
+ return parse(readFileSync(file, 'utf8'));
153
+ },
154
+ save(auth) {
155
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
156
+ writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
157
+ chmodSync(file, 0o600);
158
+ },
159
+ clear() {
160
+ rmSync(file, { force: true });
161
+ },
162
+ };
163
+ }
164
+ function parse(raw) {
165
+ try {
166
+ const v = JSON.parse(raw);
167
+ if (typeof v.access_token !== 'string' ||
168
+ typeof v.api_base !== 'string' ||
169
+ typeof v.client_id !== 'string')
170
+ return null;
171
+ return v;
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
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);
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,16 @@
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
+ nodeVersion?: string;
12
+ }
13
+ export declare function runDoctor(args: ParsedArgs, out: Reporter, deps?: DoctorDeps): Promise<number>;
14
+ export declare function collectChecks(dir: string, opts: {
15
+ offline: boolean;
16
+ }, deps: DoctorDeps): Promise<Check[]>;