atmn 0.0.1 → 0.0.3
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 +21 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +63 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +26 -0
- package/dist/commands/pull.d.ts +3 -0
- package/dist/commands/pull.js +28 -0
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +47 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +55 -0
- package/dist/core/api.d.ts +27 -0
- package/dist/core/api.js +75 -0
- package/dist/core/auth.d.ts +1 -0
- package/dist/core/auth.js +8 -0
- package/dist/core/builders/features.d.ts +2 -0
- package/dist/core/builders/features.js +10 -0
- package/dist/core/builders/products.d.ts +7 -0
- package/dist/core/builders/products.js +71 -0
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.js +25 -0
- package/dist/core/pull.d.ts +17 -0
- package/dist/core/pull.js +22 -0
- package/dist/core/push.d.ts +7 -0
- package/dist/core/push.js +67 -0
- package/dist/core/utils.d.ts +3 -0
- package/dist/core/utils.js +26 -0
- package/package.json +63 -10
- package/readme.md +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 John Yeo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import Init from './commands/init.js';
|
|
4
|
+
import { loadAutumnConfigFile } from './core/config.js';
|
|
5
|
+
import Push from './commands/push.js';
|
|
6
|
+
import Pull from './commands/pull.js';
|
|
7
|
+
import AuthCommand from './commands/auth.js';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { writeConfig } from './core/config.js';
|
|
11
|
+
import { FRONTEND_URL } from './constants.js';
|
|
12
|
+
import { DEFAULT_CONFIG } from './constants.js';
|
|
13
|
+
const VERSION = '1.0.0b';
|
|
14
|
+
program
|
|
15
|
+
.command('push')
|
|
16
|
+
.description('Push changes to Autumn')
|
|
17
|
+
.option('-p, --prod', 'Push to production')
|
|
18
|
+
.option('-y, --yes', 'Confirm all deletions')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
const config = await loadAutumnConfigFile();
|
|
21
|
+
await Push({ config, yes: options.yes, prod: options.prod });
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command('pull')
|
|
25
|
+
.description('Pull changes from Autumn')
|
|
26
|
+
.option('-p, --prod', 'Pull from production')
|
|
27
|
+
.action(async () => {
|
|
28
|
+
const config = await loadAutumnConfigFile();
|
|
29
|
+
await Pull({ config });
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Initialize an Autumn project.')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
writeConfig(DEFAULT_CONFIG); // just write an empty config to make the config file.
|
|
36
|
+
const config = await loadAutumnConfigFile();
|
|
37
|
+
await Init({ config });
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command('login')
|
|
41
|
+
.description('Authenticate with Autumn')
|
|
42
|
+
.option('-p, --prod', 'Authenticate with production')
|
|
43
|
+
.action(async () => {
|
|
44
|
+
await AuthCommand();
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command('dashboard')
|
|
48
|
+
.description('Open the Autumn dashboard in your browser')
|
|
49
|
+
.action(() => {
|
|
50
|
+
open(`${FRONTEND_URL}`);
|
|
51
|
+
});
|
|
52
|
+
program
|
|
53
|
+
.command('version')
|
|
54
|
+
.description('Show the version of Autumn')
|
|
55
|
+
.action(() => {
|
|
56
|
+
console.log(chalk.green(`Autumn v${VERSION}`));
|
|
57
|
+
});
|
|
58
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function AuthCommand(): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import open from 'open';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { input, password, confirm } from '@inquirer/prompts';
|
|
4
|
+
import { storeToEnv, readFromEnv } from '../core/utils.js';
|
|
5
|
+
import { getOTP } from '../core/auth.js';
|
|
6
|
+
import { updateCLIStripeKeys } from '../core/api.js';
|
|
7
|
+
import { FRONTEND_URL } from '../constants.js';
|
|
8
|
+
const passwordTheme = {
|
|
9
|
+
style: {
|
|
10
|
+
answer: (text) => {
|
|
11
|
+
return chalk.magenta('*'.repeat(text.length));
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const inputTheme = {
|
|
16
|
+
style: {
|
|
17
|
+
answer: (text) => {
|
|
18
|
+
return chalk.magenta(text);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export default async function AuthCommand() {
|
|
23
|
+
if (readFromEnv()) {
|
|
24
|
+
let shouldReauth = await confirm({
|
|
25
|
+
message: 'You are already authenticated. Would you like to re-authenticate?',
|
|
26
|
+
theme: inputTheme,
|
|
27
|
+
});
|
|
28
|
+
if (!shouldReauth) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
open(`${FRONTEND_URL}/dev/cli`);
|
|
33
|
+
const otp = await input({
|
|
34
|
+
message: 'Enter OTP:',
|
|
35
|
+
theme: inputTheme,
|
|
36
|
+
});
|
|
37
|
+
const keyInfo = await getOTP(otp);
|
|
38
|
+
if (!keyInfo.stripe_connected) {
|
|
39
|
+
let connectStripe = await confirm({
|
|
40
|
+
message: "It seems like your organization doesn't have any Stripe keys connected. Would you like to connect them now?",
|
|
41
|
+
theme: inputTheme,
|
|
42
|
+
});
|
|
43
|
+
if (connectStripe) {
|
|
44
|
+
// Ask for stripe Keys
|
|
45
|
+
let stripeTestKey = await password({
|
|
46
|
+
message: 'Enter Stripe Test Secret Key:',
|
|
47
|
+
mask: '*',
|
|
48
|
+
theme: passwordTheme,
|
|
49
|
+
});
|
|
50
|
+
// let stripeLiveKey = await password({
|
|
51
|
+
// message: 'Enter Stripe Live Key:',
|
|
52
|
+
// mask: '*',
|
|
53
|
+
// theme: passwordTheme,
|
|
54
|
+
// })
|
|
55
|
+
await updateCLIStripeKeys(stripeTestKey, stripeTestKey, keyInfo.stripeFlowAuthKey);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(chalk.yellow("Okay, no worries. Go to the Autumn dashboard when you're ready!"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
storeToEnv(keyInfo.prodKey, keyInfo.sandboxKey);
|
|
62
|
+
console.log(chalk.green('Success! Keys have been stored locally. You may now use the CLI.'));
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Pull from './pull.js';
|
|
3
|
+
import AuthCommand from './auth.js';
|
|
4
|
+
import { readFromEnv } from '../core/utils.js';
|
|
5
|
+
export default async function Init({ config }) {
|
|
6
|
+
// Try to read API key from .env first
|
|
7
|
+
let apiKey = readFromEnv();
|
|
8
|
+
if (apiKey) {
|
|
9
|
+
console.log(chalk.green('API key found. Pulling latest config...'));
|
|
10
|
+
await Pull({ config });
|
|
11
|
+
console.log(chalk.green('Project initialized and config pulled successfully!'));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// If not found, run authentication
|
|
15
|
+
console.log(chalk.yellow('No API key found. Running authentication...'));
|
|
16
|
+
await AuthCommand();
|
|
17
|
+
// After authentication, try to read the key again
|
|
18
|
+
apiKey = readFromEnv();
|
|
19
|
+
if (apiKey) {
|
|
20
|
+
await Pull({ config });
|
|
21
|
+
console.log(chalk.green('Project initialized! You are now authenticated and config has been pulled.'));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(chalk.red('Authentication did not yield an API key. Please check your setup.'));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getAllProducts, getFeatures, } from '../core/pull.js';
|
|
3
|
+
import { productBuilder } from '../core/builders/products.js';
|
|
4
|
+
import { featureBuilder } from '../core/builders/features.js';
|
|
5
|
+
import { writeConfig } from '../core/config.js';
|
|
6
|
+
import { importBuilder, exportBuilder } from '../core/builders/products.js';
|
|
7
|
+
import { snakeCaseToCamelCase } from '../core/utils.js';
|
|
8
|
+
export default async function Pull({ config }) {
|
|
9
|
+
console.log(chalk.green('Pulling products and features from Autumn...'));
|
|
10
|
+
const products = await getAllProducts();
|
|
11
|
+
const features = await getFeatures();
|
|
12
|
+
const productSnippets = products.map(product => productBuilder(product));
|
|
13
|
+
const featureSnippets = features.map(feature => featureBuilder(feature));
|
|
14
|
+
const autumnConfig = `
|
|
15
|
+
${importBuilder()}
|
|
16
|
+
|
|
17
|
+
// Features
|
|
18
|
+
${featureSnippets.join('\n')}
|
|
19
|
+
|
|
20
|
+
// Products
|
|
21
|
+
${productSnippets.join('\n')}
|
|
22
|
+
|
|
23
|
+
// Remember to update this when you make changes!
|
|
24
|
+
${exportBuilder(products.map(product => product.id), features.map(feature => snakeCaseToCamelCase(feature.id)))}
|
|
25
|
+
`;
|
|
26
|
+
writeConfig(autumnConfig);
|
|
27
|
+
console.log(chalk.green('Success! Config has been updated.'));
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { upsertProduct, checkForDeletables, upsertFeature, } from '../core/push.js';
|
|
4
|
+
import { deleteFeature, deleteProduct } from '../core/api.js';
|
|
5
|
+
import { FRONTEND_URL } from '../constants.js';
|
|
6
|
+
export default async function Push({ config, yes, prod, }) {
|
|
7
|
+
let { features, products } = config;
|
|
8
|
+
let { featuresToDelete, productsToDelete } = await checkForDeletables(features, products);
|
|
9
|
+
for (let productId of productsToDelete) {
|
|
10
|
+
let shouldDelete = yes ||
|
|
11
|
+
(await confirm({
|
|
12
|
+
message: `Delete product [${productId}]?`,
|
|
13
|
+
}));
|
|
14
|
+
if (shouldDelete) {
|
|
15
|
+
await deleteProduct(productId);
|
|
16
|
+
console.log(chalk.green(`Product [${productId}] deleted successfully!`));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (let feature of features) {
|
|
20
|
+
console.log(chalk.green(`Pushing feature [${feature.id}]`));
|
|
21
|
+
await upsertFeature(feature);
|
|
22
|
+
console.log(chalk.green(`Pushed feature [${feature.id}]`));
|
|
23
|
+
}
|
|
24
|
+
for (let product of products) {
|
|
25
|
+
console.log(chalk.green(`Pushing product [${product.id}]`));
|
|
26
|
+
await upsertProduct(product);
|
|
27
|
+
console.log(chalk.green(`Pushed product [${product.id}]`));
|
|
28
|
+
}
|
|
29
|
+
for (let featureId of featuresToDelete) {
|
|
30
|
+
let shouldDelete = yes ||
|
|
31
|
+
(await confirm({
|
|
32
|
+
message: `Delete feature [${featureId}]?`,
|
|
33
|
+
}));
|
|
34
|
+
if (shouldDelete) {
|
|
35
|
+
await deleteFeature(featureId);
|
|
36
|
+
console.log(chalk.green(`Feature [${featureId}] deleted successfully!`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const env = prod ? 'prod' : 'sandbox';
|
|
40
|
+
console.log(chalk.magentaBright(`Success! Changes have been pushed to ${env}.`));
|
|
41
|
+
if (prod) {
|
|
42
|
+
console.log(chalk.magentaBright(`You can view the products at ${FRONTEND_URL}/products`));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.log(chalk.magentaBright(`You can view the products at ${FRONTEND_URL}/sandbox/products`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const FRONTEND_URL = "http://app.useautumn.com";
|
|
2
|
+
export declare const BACKEND_URL = "https://api.useautumn.com";
|
|
3
|
+
export declare const DEFAULT_CONFIG = "import {\n\tfeature,\n\tproduct,\n\tpriceItem,\n\tfeatureItem,\n\tpricedFeatureItem,\n} from 'autumn-js/compose';\n\nconst seats = feature({\n\tid: 'seats',\n\tname: 'Seats',\n\ttype: 'continuous_use',\n});\n\nconst messages = feature({\n\tid: 'messages',\n\tname: 'Messages',\n\ttype: 'single_use',\n});\n\nconst pro = product({\n\tid: 'pro',\n\tname: 'Pro',\n\titems: [\n\t\t// 500 messages per month\n\t\tfeatureItem({\n\t\t\tfeature_id: messages.id,\n\t\t\tincluded_usage: 500,\n\t\t\tinterval: 'month',\n\t\t}),\n\n\t\t// $10 per seat per month\n\t\tpricedFeatureItem({\n\t\t\tfeature_id: seats.id,\n\t\t\tprice: 10,\n\t\t\tinterval: 'month',\n\t\t}),\n\n\t\t// $50 / month\n\t\tpriceItem({\n\t\t\tprice: 50,\n\t\t\tinterval: 'month',\n\t\t}),\n\t],\n});\n\nexport default {\n\tfeatures: [seats, messages],\n\tproducts: [pro],\n};\n";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// export const FRONTEND_URL = 'http://localhost:3000';
|
|
2
|
+
// export const BACKEND_URL = 'http://localhost:8080';
|
|
3
|
+
export const FRONTEND_URL = 'http://app.useautumn.com';
|
|
4
|
+
export const BACKEND_URL = 'https://api.useautumn.com';
|
|
5
|
+
export const DEFAULT_CONFIG = `import {
|
|
6
|
+
feature,
|
|
7
|
+
product,
|
|
8
|
+
priceItem,
|
|
9
|
+
featureItem,
|
|
10
|
+
pricedFeatureItem,
|
|
11
|
+
} from 'autumn-js/compose';
|
|
12
|
+
|
|
13
|
+
const seats = feature({
|
|
14
|
+
id: 'seats',
|
|
15
|
+
name: 'Seats',
|
|
16
|
+
type: 'continuous_use',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const messages = feature({
|
|
20
|
+
id: 'messages',
|
|
21
|
+
name: 'Messages',
|
|
22
|
+
type: 'single_use',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const pro = product({
|
|
26
|
+
id: 'pro',
|
|
27
|
+
name: 'Pro',
|
|
28
|
+
items: [
|
|
29
|
+
// 500 messages per month
|
|
30
|
+
featureItem({
|
|
31
|
+
feature_id: messages.id,
|
|
32
|
+
included_usage: 500,
|
|
33
|
+
interval: 'month',
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
// $10 per seat per month
|
|
37
|
+
pricedFeatureItem({
|
|
38
|
+
feature_id: seats.id,
|
|
39
|
+
price: 10,
|
|
40
|
+
interval: 'month',
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// $50 / month
|
|
44
|
+
priceItem({
|
|
45
|
+
price: 50,
|
|
46
|
+
interval: 'month',
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
features: [seats, messages],
|
|
53
|
+
products: [pro],
|
|
54
|
+
};
|
|
55
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare function request({ method, base, path, data, headers, customAuth, throwOnError, }: {
|
|
2
|
+
method: string;
|
|
3
|
+
base: string;
|
|
4
|
+
path: string;
|
|
5
|
+
data?: any;
|
|
6
|
+
headers?: any;
|
|
7
|
+
customAuth?: string;
|
|
8
|
+
throwOnError?: boolean;
|
|
9
|
+
}): Promise<any>;
|
|
10
|
+
export declare function internalRequest({ method, path, data, headers, customAuth, }: {
|
|
11
|
+
method: string;
|
|
12
|
+
path: string;
|
|
13
|
+
data?: any;
|
|
14
|
+
headers?: any;
|
|
15
|
+
customAuth?: string;
|
|
16
|
+
}): Promise<any>;
|
|
17
|
+
export declare function externalRequest({ method, path, data, headers, customAuth, throwOnError, }: {
|
|
18
|
+
method: string;
|
|
19
|
+
path: string;
|
|
20
|
+
data?: any;
|
|
21
|
+
headers?: any;
|
|
22
|
+
customAuth?: string;
|
|
23
|
+
throwOnError?: boolean;
|
|
24
|
+
}): Promise<any>;
|
|
25
|
+
export declare function deleteFeature(id: string): Promise<any>;
|
|
26
|
+
export declare function deleteProduct(id: string): Promise<any>;
|
|
27
|
+
export declare function updateCLIStripeKeys(stripeTestKey: string, stripeLiveKey: string, stripeFlowAuthKey: string): Promise<any>;
|
package/dist/core/api.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BACKEND_URL } from '../constants.js';
|
|
4
|
+
import { readFromEnv } from './utils.js';
|
|
5
|
+
const INTERNAL_BASE = BACKEND_URL;
|
|
6
|
+
const EXTERNAL_BASE = `${BACKEND_URL}/v1`;
|
|
7
|
+
export async function request({ method, base, path, data, headers, customAuth, throwOnError = true, }) {
|
|
8
|
+
const apiKey = readFromEnv();
|
|
9
|
+
try {
|
|
10
|
+
const response = await axios.request({
|
|
11
|
+
method,
|
|
12
|
+
url: `${base}${path}`,
|
|
13
|
+
data,
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
...headers,
|
|
17
|
+
Authorization: customAuth || `Bearer ${apiKey}`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return response.data;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (throwOnError) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
console.error(chalk.red('Error occured when making API request:'), chalk.red(error.response.data.message || error.response.data.error));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function internalRequest({ method, path, data, headers, customAuth, }) {
|
|
31
|
+
return await request({
|
|
32
|
+
method,
|
|
33
|
+
base: INTERNAL_BASE,
|
|
34
|
+
path,
|
|
35
|
+
data,
|
|
36
|
+
headers,
|
|
37
|
+
customAuth,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function externalRequest({ method, path, data, headers, customAuth, throwOnError = false, }) {
|
|
41
|
+
return await request({
|
|
42
|
+
method,
|
|
43
|
+
base: EXTERNAL_BASE,
|
|
44
|
+
path,
|
|
45
|
+
data,
|
|
46
|
+
headers,
|
|
47
|
+
customAuth,
|
|
48
|
+
throwOnError,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function deleteFeature(id) {
|
|
52
|
+
return await externalRequest({
|
|
53
|
+
method: 'DELETE',
|
|
54
|
+
path: `/features/${id}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function deleteProduct(id) {
|
|
58
|
+
return await externalRequest({
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
path: `/products/${id}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function updateCLIStripeKeys(stripeTestKey, stripeLiveKey, stripeFlowAuthKey) {
|
|
64
|
+
return await internalRequest({
|
|
65
|
+
method: 'POST',
|
|
66
|
+
path: '/dev/cli/stripe',
|
|
67
|
+
data: {
|
|
68
|
+
stripeTestKey,
|
|
69
|
+
stripeLiveKey,
|
|
70
|
+
successUrl: 'https://useautumn.com',
|
|
71
|
+
defaultCurrency: 'usd',
|
|
72
|
+
},
|
|
73
|
+
customAuth: stripeFlowAuthKey,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getOTP(otp: string): Promise<any>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { snakeCaseToCamelCase } from '../utils.js';
|
|
2
|
+
export function featureBuilder(feature) {
|
|
3
|
+
const snippet = `
|
|
4
|
+
export const ${snakeCaseToCamelCase(feature.id)} = feature({
|
|
5
|
+
id: '${feature.id}',
|
|
6
|
+
name: '${feature.name}',
|
|
7
|
+
type: '${feature.type}',
|
|
8
|
+
})`;
|
|
9
|
+
return snippet;
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ProductItem, Product } from 'autumn-js/compose';
|
|
2
|
+
export declare function importBuilder(): string;
|
|
3
|
+
export declare function exportBuilder(productIds: string[], featureIds: string[]): string;
|
|
4
|
+
export declare function productBuilder(product: Product): string;
|
|
5
|
+
export declare function pricedFeatureItemBuilder(item: ProductItem): string;
|
|
6
|
+
export declare function featureItemBuilder(item: ProductItem): string;
|
|
7
|
+
export declare function priceItemBuilder(item: ProductItem): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { snakeCaseToCamelCase } from '../utils.js';
|
|
2
|
+
const ItemBuilders = {
|
|
3
|
+
priced_feature: pricedFeatureItemBuilder,
|
|
4
|
+
feature: featureItemBuilder,
|
|
5
|
+
price: priceItemBuilder,
|
|
6
|
+
};
|
|
7
|
+
export function importBuilder() {
|
|
8
|
+
return `
|
|
9
|
+
import {
|
|
10
|
+
feature,
|
|
11
|
+
product,
|
|
12
|
+
featureItem,
|
|
13
|
+
pricedFeatureItem,
|
|
14
|
+
priceItem,
|
|
15
|
+
} from 'autumn-js/compose';
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
export function exportBuilder(productIds, featureIds) {
|
|
19
|
+
const snippet = `
|
|
20
|
+
export default {
|
|
21
|
+
products: [${productIds.map(id => `${id}Plan`).join(', ')}],
|
|
22
|
+
features: [${featureIds.map(id => `${id}`).join(', ')}]
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
return snippet;
|
|
26
|
+
}
|
|
27
|
+
export function productBuilder(product) {
|
|
28
|
+
const snippet = `
|
|
29
|
+
export const ${product.id}Plan = product({
|
|
30
|
+
id: '${product.id}',
|
|
31
|
+
name: '${product.name}',
|
|
32
|
+
items: [${product.items
|
|
33
|
+
.map((item) => `${ItemBuilders[item.type](item)}`)
|
|
34
|
+
.join(' ')} ]
|
|
35
|
+
})
|
|
36
|
+
`;
|
|
37
|
+
return snippet;
|
|
38
|
+
}
|
|
39
|
+
// Item Builders
|
|
40
|
+
export function pricedFeatureItemBuilder(item) {
|
|
41
|
+
const intervalLine = item.interval == null ? '' : `\n interval: '${item.interval}',`;
|
|
42
|
+
const snippet = `
|
|
43
|
+
pricedFeatureItem({
|
|
44
|
+
feature_id: ${snakeCaseToCamelCase(item.feature_id)}.id,
|
|
45
|
+
price: ${item.price},${intervalLine}
|
|
46
|
+
included_usage: ${item.included_usage},
|
|
47
|
+
billing_units: ${item.billing_units},
|
|
48
|
+
usage_model: '${item.usage_model}',
|
|
49
|
+
}),
|
|
50
|
+
`;
|
|
51
|
+
return snippet;
|
|
52
|
+
}
|
|
53
|
+
export function featureItemBuilder(item) {
|
|
54
|
+
const intervalLine = item.interval == null ? '' : `\n interval: '${item.interval}',`;
|
|
55
|
+
const snippet = `
|
|
56
|
+
featureItem({
|
|
57
|
+
feature_id: ${snakeCaseToCamelCase(item.feature_id)}.id,
|
|
58
|
+
included_usage: ${item.included_usage},${intervalLine}
|
|
59
|
+
}),
|
|
60
|
+
`;
|
|
61
|
+
return snippet;
|
|
62
|
+
}
|
|
63
|
+
export function priceItemBuilder(item) {
|
|
64
|
+
const intervalLine = item.interval == null ? '' : `\n interval: '${item.interval}',`;
|
|
65
|
+
const snippet = `
|
|
66
|
+
priceItem({
|
|
67
|
+
price: ${item.price},${intervalLine}
|
|
68
|
+
}),
|
|
69
|
+
`;
|
|
70
|
+
return snippet;
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import createJiti from 'jiti';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
export async function loadAutumnConfigFile() {
|
|
7
|
+
const configPath = path.join(process.cwd(), 'autumn.config.ts');
|
|
8
|
+
const absolutePath = resolve(configPath);
|
|
9
|
+
const fileUrl = pathToFileURL(absolutePath).href;
|
|
10
|
+
// Dynamic import the TypeScript config file
|
|
11
|
+
const jiti = createJiti(import.meta.url);
|
|
12
|
+
const mod = await jiti.import(fileUrl);
|
|
13
|
+
const def = mod.default || mod;
|
|
14
|
+
if (!def.products || !Array.isArray(def.products)) {
|
|
15
|
+
throw new Error("You must export a products field that is an array of products.");
|
|
16
|
+
}
|
|
17
|
+
if (!def.features || !Array.isArray(def.features)) {
|
|
18
|
+
throw new Error("You must export a features field that is an array of products.");
|
|
19
|
+
}
|
|
20
|
+
return def;
|
|
21
|
+
}
|
|
22
|
+
export function writeConfig(config) {
|
|
23
|
+
const configPath = path.join(process.cwd(), 'autumn.config.ts');
|
|
24
|
+
fs.writeFileSync(configPath, config);
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function getProducts(ids: string[]): Promise<{
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
items: {
|
|
5
|
+
type?: "feature" | "priced_feature" | null | undefined;
|
|
6
|
+
feature_id?: string | null | undefined;
|
|
7
|
+
included_usage?: number | "inf" | null | undefined;
|
|
8
|
+
interval?: "minute" | "hour" | "day" | "week" | "month" | "quarter" | "semi_annual" | "year" | null | undefined;
|
|
9
|
+
usage_model?: "prepaid" | "pay_per_use" | null | undefined;
|
|
10
|
+
price?: number | null | undefined;
|
|
11
|
+
billing_units?: number | null | undefined;
|
|
12
|
+
}[];
|
|
13
|
+
is_add_on?: boolean | undefined;
|
|
14
|
+
is_default?: boolean | undefined;
|
|
15
|
+
}[]>;
|
|
16
|
+
export declare function getAllProducts(): Promise<any>;
|
|
17
|
+
export declare function getFeatures(): Promise<any>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { externalRequest } from './api.js';
|
|
2
|
+
export async function getProducts(ids) {
|
|
3
|
+
const products = await Promise.all(ids.map(id => externalRequest({
|
|
4
|
+
method: 'GET',
|
|
5
|
+
path: `/products/${id}`,
|
|
6
|
+
})));
|
|
7
|
+
return products.map(product => product);
|
|
8
|
+
}
|
|
9
|
+
export async function getAllProducts() {
|
|
10
|
+
const { list } = await externalRequest({
|
|
11
|
+
method: 'GET',
|
|
12
|
+
path: '/products',
|
|
13
|
+
});
|
|
14
|
+
return list.map(product => product);
|
|
15
|
+
}
|
|
16
|
+
export async function getFeatures() {
|
|
17
|
+
const { list } = await externalRequest({
|
|
18
|
+
method: 'GET',
|
|
19
|
+
path: '/features',
|
|
20
|
+
});
|
|
21
|
+
return list.map(feature => feature);
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Feature, Product } from 'autumn-js/compose';
|
|
2
|
+
export declare function checkForDeletables(currentFeatures: Feature[], currentProducts: Product[]): Promise<{
|
|
3
|
+
featuresToDelete: any;
|
|
4
|
+
productsToDelete: any;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function upsertFeature(feature: Feature): Promise<any>;
|
|
7
|
+
export declare function upsertProduct(product: Product): Promise<any>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { externalRequest } from './api.js';
|
|
2
|
+
import { getFeatures, getAllProducts } from './pull.js';
|
|
3
|
+
export async function checkForDeletables(currentFeatures, currentProducts) {
|
|
4
|
+
const features = await getFeatures(); // Get from AUTUMN
|
|
5
|
+
const featureIds = features.map(feature => feature.id);
|
|
6
|
+
const currentFeatureIds = currentFeatures.map(feature => feature.id);
|
|
7
|
+
const featuresToDelete = featureIds.filter(featureId => !currentFeatureIds.includes(featureId));
|
|
8
|
+
const products = await getAllProducts();
|
|
9
|
+
const productIds = products.map(product => product.id);
|
|
10
|
+
const currentProductIds = currentProducts.map(product => product.id);
|
|
11
|
+
const productsToDelete = productIds.filter(productId => !currentProductIds.includes(productId));
|
|
12
|
+
return { featuresToDelete, productsToDelete };
|
|
13
|
+
}
|
|
14
|
+
export async function upsertFeature(feature) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await externalRequest({
|
|
17
|
+
method: 'POST',
|
|
18
|
+
path: `/features`,
|
|
19
|
+
data: feature,
|
|
20
|
+
throwOnError: true,
|
|
21
|
+
// data: {
|
|
22
|
+
// ...feature,
|
|
23
|
+
// config: {
|
|
24
|
+
// filters: [{property: '', operator: '', value: []}],
|
|
25
|
+
// usage_type: 'single_use',
|
|
26
|
+
// },
|
|
27
|
+
// },
|
|
28
|
+
});
|
|
29
|
+
return response.data;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// If the first request fails, try posting to the specific feature ID endpoint
|
|
33
|
+
const response = await externalRequest({
|
|
34
|
+
method: 'POST',
|
|
35
|
+
path: `/features/${feature.id}`,
|
|
36
|
+
data: feature,
|
|
37
|
+
// data: {
|
|
38
|
+
// ...feature,
|
|
39
|
+
// config: {
|
|
40
|
+
// filters: [{property: '', operator: '', value: []}],
|
|
41
|
+
// usage_type: 'single_use',
|
|
42
|
+
// },
|
|
43
|
+
// },
|
|
44
|
+
});
|
|
45
|
+
return response.data;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function upsertProduct(product) {
|
|
49
|
+
try {
|
|
50
|
+
const response = await externalRequest({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: `/products`,
|
|
53
|
+
data: product,
|
|
54
|
+
throwOnError: true,
|
|
55
|
+
});
|
|
56
|
+
return response.data;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// If the first request fails, try posting to the specific product ID endpoint
|
|
60
|
+
const response = await externalRequest({
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: `/products/${product.id}`,
|
|
63
|
+
data: product,
|
|
64
|
+
});
|
|
65
|
+
return response.data;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
export function snakeCaseToCamelCase(value) {
|
|
4
|
+
return value.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
export function storeToEnv(prodKey, sandboxKey) {
|
|
7
|
+
const envPath = `${process.cwd()}/.env`;
|
|
8
|
+
const envVars = `# AUTUMN_SECRET_KEY=${prodKey}\nAUTUMN_SECRET_KEY=${sandboxKey}\n`;
|
|
9
|
+
if (fs.existsSync(envPath)) {
|
|
10
|
+
fs.appendFileSync(envPath, envVars);
|
|
11
|
+
console.log(chalk.green('.env file found. Appended keys.'));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
fs.writeFileSync(envPath, envVars);
|
|
15
|
+
console.log(chalk.green('.env file not found. Created new .env file and wrote keys.'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function readFromEnv() {
|
|
19
|
+
const envPath = `${process.cwd()}/.env`;
|
|
20
|
+
if (!fs.existsSync(envPath)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
24
|
+
const match = envContent.match(/^AUTUMN_SECRET_KEY=(.*)$/m);
|
|
25
|
+
return match ? match[1] : undefined;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"name": "atmn",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"bin": "dist/cli.js",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=16"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test2": "prettier --check . && xo && ava",
|
|
15
|
+
"test": "node ./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@inquirer/prompts": "^7.6.0",
|
|
24
|
+
"autumn-js": "workspace:*",
|
|
25
|
+
"axios": "^1.10.0",
|
|
26
|
+
"commander": "^14.0.0",
|
|
27
|
+
"dotenv": "^17.2.0",
|
|
28
|
+
"inquirer": "^12.7.0",
|
|
29
|
+
"jiti": "^2.4.2",
|
|
30
|
+
"open": "^10.1.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
34
|
+
"@types/node": "^24.0.10",
|
|
35
|
+
"@types/react": "^18.0.32",
|
|
36
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
37
|
+
"ava": "^5.2.0",
|
|
38
|
+
"chalk": "^5.2.0",
|
|
39
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
40
|
+
"eslint-plugin-react": "^7.32.2",
|
|
41
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
42
|
+
"ink-testing-library": "^3.0.0",
|
|
43
|
+
"prettier": "^2.8.7",
|
|
44
|
+
"ts-node": "^10.9.1",
|
|
45
|
+
"typescript": "^5.0.3",
|
|
46
|
+
"xo": "^0.53.1"
|
|
47
|
+
},
|
|
48
|
+
"ava": {
|
|
49
|
+
"extensions": {
|
|
50
|
+
"ts": "module",
|
|
51
|
+
"tsx": "module"
|
|
52
|
+
},
|
|
53
|
+
"nodeArguments": [
|
|
54
|
+
"--loader=ts-node/esm"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"xo": {
|
|
58
|
+
"extends": "xo-react",
|
|
59
|
+
"prettier": true,
|
|
60
|
+
"rules": {
|
|
61
|
+
"react/prop-types": "off"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"prettier": "@vdemedes/prettier-config"
|
|
12
65
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Autumn CLI
|
|
2
|
+
|
|
3
|
+
The CLI tool for [Autumn](https://useautumn.com)'s REST API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
Features
|
|
7
|
+
|
|
8
|
+
- Create your features in products **in-code**.
|
|
9
|
+
- Authenticate with the CLI tool.
|
|
10
|
+
- Easily push and pull changes to and from Autumn.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
Usage: atmn [options] [command]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-h, --help display help for command
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
push [options] Push changes to Autumn
|
|
22
|
+
pull [options] Pull changes from Autumn
|
|
23
|
+
init Initialize an Autumn project.
|
|
24
|
+
auth [options] Authenticate with Autumn
|
|
25
|
+
dashboard Open the Autumn dashboard in your browser
|
|
26
|
+
version Show the version of Autumn
|
|
27
|
+
help [command] display help for command
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Getting started
|
|
31
|
+
Run `npx atmn init` in your project folder. This will prompt you to authenticate with Autumn and
|
|
32
|
+
downloads your Autumn keys to the root level `.env`.
|
|
33
|
+
|
|
34
|
+
If you make a change locally in your `autumn.config.ts` file and you'd like to push, simply run the `push` command:
|
|
35
|
+
```sh
|
|
36
|
+
npx atmn push
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Likewise, to pull changes from Autumn, run the `pull` command:
|
|
40
|
+
```sh
|
|
41
|
+
npx atmn pull
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you'd like to access your production environment, go to your `.env` and uncomment out the
|
|
45
|
+
production key, and comment out your sandbox key.
|
|
46
|
+
|
|
47
|
+
## Example `autumn.config.ts`
|
|
48
|
+
|
|
49
|
+
```typescript autumn.config.ts
|
|
50
|
+
import {
|
|
51
|
+
feature,
|
|
52
|
+
product,
|
|
53
|
+
priceItem,
|
|
54
|
+
featureItem,
|
|
55
|
+
pricedFeatureItem,
|
|
56
|
+
} from 'autumn-js/compose';
|
|
57
|
+
|
|
58
|
+
const seats = feature({
|
|
59
|
+
id: 'seats',
|
|
60
|
+
name: 'Seats',
|
|
61
|
+
type: 'continuous_use',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const messages = feature({
|
|
65
|
+
id: 'messages',
|
|
66
|
+
name: 'Messages',
|
|
67
|
+
type: 'single_use',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const pro = product({
|
|
71
|
+
id: 'pro',
|
|
72
|
+
name: 'Pro',
|
|
73
|
+
items: [
|
|
74
|
+
// 500 messages per month
|
|
75
|
+
featureItem({
|
|
76
|
+
feature_id: messages.id,
|
|
77
|
+
included_usage: 500,
|
|
78
|
+
interval: 'month',
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
// $10 per seat per month
|
|
82
|
+
pricedFeatureItem({
|
|
83
|
+
feature_id: seats.id,
|
|
84
|
+
price: 10,
|
|
85
|
+
interval: 'month',
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
// $50 / month
|
|
89
|
+
priceItem({
|
|
90
|
+
price: 50,
|
|
91
|
+
interval: 'month',
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export default {
|
|
97
|
+
features: [seats, messages],
|
|
98
|
+
products: [pro],
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|