Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
1 change: 0 additions & 1 deletion .prettierrc.json

This file was deleted.

9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"eslint.format.enable": true,
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
npm install -g adapty
```

Requires Node.js >= 18.
Requires Node.js 22 or 24. Node 18 and 20 are past end-of-life and are not supported.

## Authentication

Expand Down
4 changes: 2 additions & 2 deletions bin/dev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning

import {execute} from '@oclif/core'
import { execute } from '@oclif/core';

await execute({development: true, dir: import.meta.url})
await execute({ development: true, dir: import.meta.url });
4 changes: 2 additions & 2 deletions bin/run.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node

import {execute} from '@oclif/core'
import { execute } from '@oclif/core';

await execute({dir: import.meta.url})
await execute({ dir: import.meta.url });
2 changes: 1 addition & 1 deletion docs/agent/skills/adapty-cli-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ the dashboard usually means the token is scoped elsewhere.

| What you see | What it means | What to do |
| --- | --- | --- |
| `ERROR node_missing` / `node_too_old` | Node.js below 18 | Install Node 18+; nothing else will work |
| `ERROR node_missing` / `node_too_old` | Node.js below 22 | Install Node 22+; nothing else will work |
| `ERROR npm_install_failed`, log shows a network, DNS or registry error | **In Cowork or any sandbox: egress is off, or the domains are not allowlisted.** The most common cloud failure, and not fixable from the shell | Settings → Capabilities → enable code execution → allow network egress → an access mode that permits package managers → add **both** `adapty.io` and `*.adapty.io` (a wildcard does not cover the apex domain). **Settings apply when a task starts**, so after changing them the user must start a new task; changing them mid-conversation does nothing |
| `ERROR npm_install_failed`, log shows `EACCES` or a write error | Install failed even with a user prefix | Read `$TMPDIR/adapty-setup/npm.log`. Never re-run under `sudo` |
| `ERROR adapty_not_on_path` | Installed, but the global bin dir is not on `PATH` | Export the path the error prints |
Expand Down
251 changes: 230 additions & 21 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,22 +1,231 @@
import {includeIgnoreFile} from '@eslint/compat'
import oclif from 'eslint-config-oclif'
import prettier from 'eslint-config-prettier'
import path from 'node:path'
import {fileURLToPath} from 'node:url'

const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore')

export default [
includeIgnoreFile(gitignorePath),
...oclif,
prettier,
{
rules: {
camelcase: ['error', {properties: 'never'}],
'n/no-process-exit': 'off',
'n/no-unsupported-features/node-builtins': 'off',
'no-await-in-loop': 'off',
'unicorn/no-process-exit': 'off',
// @ts-check
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import importX from 'eslint-plugin-import-x';
import n from 'eslint-plugin-n';
import tseslint from 'typescript-eslint';

// ── Base: plugins, parser, paths ────────────────────────────────────────────

// Every part goes through tseslint.config(): it validates the shape and infers rule
// levels as tuples instead of string[] — without it @ts-check rejects ['error', {...}]
const base = tseslint.config(
{ ignores: ['dist/**', 'reference/**'] },

{
plugins: { 'import-x': importX },

languageOptions: {
parserOptions: {
// eslint.config.mjs is outside tsconfig — lint it without types
projectService: { allowDefaultProject: ['eslint.config.mjs'] },
tsconfigRootDir: import.meta.dirname,
},
},

settings: {
// src/*.ts ships as dist/*.js — otherwise eslint-plugin-n can't find the files
n: { convertPath: { 'src/**/*.ts': ['^src/(.+)\\.ts$', 'dist/$1.js'] } },
},
},
);

// ── 1. Correctness: what breaks at runtime ──────────────────────────────────

const correctness = tseslint.config(
js.configs.recommended,
tseslint.configs.strictTypeChecked,
n.configs['flat/recommended-module'],

{
rules: {
// A forgotten await is the top source of silent bugs in a CLI
'@typescript-eslint/no-floating-promises': ['error', {
// describe/it/test from node:test register a test, they need no await
allowForKnownSafeCalls: [
{ from: 'package', package: 'node:test', name: ['describe', 'it', 'test'] },
],
}],
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/return-await': ['error', 'in-try-catch'],

'eqeqeq': ['error', 'always', { null: 'ignore' }],
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],

// Print through this.log only, or --json breaks
'no-console': 'error',
// oclif owns process exit, through error exit codes
'n/no-process-exit': 'error',
// import 'node:fs', not 'fs'
'n/prefer-node-protocol': 'error',

// The rule reads engines.node and rejects whatever Node's docs still label
// Experimental — even an API that shipped years earlier. These two run on every
// version we support; only the label moved later. Listing them here keeps a
// doc-stability marker from inflating the package's public engines contract:
// readline/promises — shipped in 17.0.0, labelled Stable in 22.17
// import.meta.dirname — shipped in 20.11.0, labelled Stable in 22.16
'n/no-unsupported-features/node-builtins': ['error', {
ignores: ['readline/promises', 'import.meta.dirname'],
}],
},
},

{
// scripts/ and bin/ aren't oclif commands: they print and set the exit code themselves
files: ['scripts/**', 'bin/**'],
rules: {
'no-console': 'off',
'n/no-process-exit': 'off',
// both launchers need a shebang, though package.json bin lists only run.js
'n/hashbang': 'off',
},
},
);

// ── 2. TypeScript conventions ───────────────────────────────────────────────

const typescript = tseslint.config(
tseslint.configs.stylisticTypeChecked,

{
rules: {
// type over interface; interface only where declarations must merge
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],

// a separate `import type { X }`, not `import { type X }`
'@typescript-eslint/consistent-type-imports': ['error', {
prefer: 'type-imports',
fixStyle: 'separate-type-imports',
}],
'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'],

// `export { type X } from './m'` leaves an empty `export {}` at runtime,
// while a separate `export type { X }` is erased completely
'@typescript-eslint/consistent-type-exports': ['error', {
fixMixedExportsWithInlineTypeSpecifier: false,
}],
'@typescript-eslint/no-import-type-side-effects': 'error',

'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
// exhaustive switch over a union — command routing
'@typescript-eslint/switch-exhaustiveness-check': 'error',
},
},
);

// ── 3. Formatting ───────────────────────────────────────────────────────────

const formatting = tseslint.config(
stylistic.configs.customize({
indent: 4,
quotes: 'single',
semi: true,
braceStyle: '1tbs',
commaDangle: 'always-multiline',
jsx: false,
}),

{
rules: {
'curly': ['error', 'all'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'@stylistic/semi': ['error', 'always'],
'@stylistic/member-delimiter-style': 'error',
'@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
'@stylistic/max-len': ['warn', {
code: 120,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
ignoreRegExpLiterals: true,
// a suppression comment can't be wrapped: the directive must sit on one line
ignorePattern: '// eslint-disable',
}],

// Multiline statements get blank lines around them, one-liners may sit together
'@stylistic/padded-blocks': ['error', 'never'],
'@stylistic/padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: 'import', next: '*' },
{ blankLine: 'any', prev: 'import', next: 'import' },
{ blankLine: 'any', prev: ['const', 'let'], next: ['const', 'let'] },
{
blankLine: 'always',
prev: '*',
next: ['multiline-const', 'multiline-let', 'multiline-expression', 'multiline-block-like'],
},
{
blankLine: 'always',
prev: ['multiline-const', 'multiline-let', 'multiline-expression', 'multiline-block-like'],
next: '*',
},
{ blankLine: 'always', prev: '*', next: 'return' },
],

'import-x/order': ['error', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type'],
'newlines-between': 'always',
'alphabetize': { order: 'asc', caseInsensitive: true },
'warnOnUnassignedImports': true,
}],
'import-x/no-duplicates': 'error',

// A nested ternary is an if/else written unreadably
'no-nested-ternary': 'error',
// `x ? true : false` and `x ? x : y` instead of Boolean(x) and x ?? y
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
// spell the cast out: Boolean(x), Number(x), String(x) — not !!x, +x, '' + x
'no-implicit-coercion': 'error',
},
},
},
]
);

// ── 4. Architecture: one dependency arrow, cli → sdk → core ─────────────────

// Patterns match the import string as written, not the resolved path: a relative
// specifier has no `sdk` segment and the number of `../` isn't known up front.
// The form without `/**` catches a barrel import of the directory.
const noOclif = {
group: ['@oclif/*'],
message: 'sdk must not depend on oclif: the framework lives in src/cli',
};

const noCli = {
group: ['**/cli', '**/cli/**'],
message: 'sdk must not import cli',
};

const noProducts = {
group: ['**/adapty', '**/adapty/**', '**/asa', '**/asa/**'],
message: 'core must not know about products',
};

// src/sdk doesn't exist yet — these rules await the layer split and match nothing today.
const architecture = tseslint.config(
{
files: ['src/sdk/**/*.ts'],
rules: { 'no-restricted-imports': ['error', { patterns: [noOclif, noCli] }] },
},

{
// A later block replaces rule options instead of merging, so sdk boundaries repeat here
files: ['src/sdk/core/**/*.ts'],
rules: { 'no-restricted-imports': ['error', { patterns: [noOclif, noCli, noProducts] }] },
},
);

export default tseslint.config(
...base,
...correctness,
...typescript,
...formatting,
...architecture,

// Configs and scripts live outside tsconfig — type-aware rules have nothing to read
{ files: ['**/*.mjs', '**/*.js'], extends: [tseslint.configs.disableTypeChecked] },
);
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,27 @@
},
"devDependencies": {
"@eslint/compat": "^1",
"@oclif/prettier-config": "^0.2.1",
"@eslint/js": "^9.39.2",
"@oclif/test": "^4",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/chai": "^4",
"@types/mocha": "^10",
"@types/node": "^18",
"@types/node": "^22",
"@types/sinon": "^21.0.0",
"chai": "^4",
"eslint": "^9",
"eslint-config-oclif": "^6",
"eslint-config-prettier": "^10",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-n": "^18.3.0",
"mocha": "^10",
"oclif": "^4",
"shx": "^0.3.3",
"sinon": "^21.0.2",
"ts-node": "^10",
"typescript": "^5"
"typescript": "^5",
"typescript-eslint": "^8.70.0"
},
"engines": {
"node": ">=18.0.0"
"node": "^22 || >=24"
},
"packageManager": "pnpm@10.28.1",
"files": [
Expand Down Expand Up @@ -137,7 +139,7 @@
"scripts": {
"build": "shx rm -rf dist && tsc -b",
"check:agent-docs": "oclif manifest && node scripts/check-agent-docs.mjs",
"lint": "eslint",
"lint": "eslint --max-warnings 0",
"postpack": "shx rm -f oclif.manifest.json",
"posttest": "pnpm run lint",
"prepack": "oclif manifest && oclif readme",
Expand Down
Loading
Loading