@slates/cli 1.0.0-rc.2

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.
@@ -0,0 +1,134 @@
1
+ import { access, readFile, readdir } from 'fs/promises';
2
+ import path from 'path';
3
+ import { resolveSlatesCliRoot } from '@slates/profiles';
4
+
5
+ export interface ResolvedIntegration {
6
+ input: string;
7
+ rootDir: string;
8
+ dirPath: string;
9
+ relativeDir: string;
10
+ name: string;
11
+ entry: string;
12
+ }
13
+
14
+ export interface WorkspaceIntegrationSummary {
15
+ rootDir: string;
16
+ dirPath: string;
17
+ relativeDir: string;
18
+ name: string;
19
+ }
20
+
21
+ let toPosixPath = (value: string) => value.replace(/\\/g, '/');
22
+
23
+ let pathExists = async (targetPath: string) => {
24
+ try {
25
+ await access(targetPath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ };
31
+
32
+ let isWithinRoot = (rootDir: string, targetPath: string) => {
33
+ let relative = path.relative(rootDir, targetPath);
34
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
35
+ };
36
+
37
+ let resolveIntegrationDir = async (input: string, cwd: string) => {
38
+ let rootDir = resolveSlatesCliRoot(cwd);
39
+ let namedPath = path.join(rootDir, 'integrations', input);
40
+ if (!input.includes(path.sep) && !input.includes('/')) {
41
+ if (await pathExists(path.join(namedPath, 'package.json'))) {
42
+ return { rootDir, dirPath: namedPath };
43
+ }
44
+ }
45
+
46
+ let candidate = path.resolve(cwd, input);
47
+ if (!(await pathExists(path.join(candidate, 'package.json')))) {
48
+ throw new Error(
49
+ `Could not resolve integration "${input}". Pass an integration name from \`integrations/\` or a relative path to an integration directory.`
50
+ );
51
+ }
52
+
53
+ if (!isWithinRoot(rootDir, candidate)) {
54
+ throw new Error(`Integration "${input}" must be inside the current repository.`);
55
+ }
56
+
57
+ return { rootDir, dirPath: candidate };
58
+ };
59
+
60
+ let resolveDefaultEntry = async (dirPath: string) => {
61
+ let packageJsonPath = path.join(dirPath, 'package.json');
62
+ let manifest = JSON.parse(await readFile(packageJsonPath, 'utf-8')) as {
63
+ main?: string;
64
+ source?: string;
65
+ };
66
+
67
+ let candidates = [
68
+ manifest.source,
69
+ manifest.main,
70
+ 'src/index.ts',
71
+ 'src/index.js',
72
+ 'index.ts',
73
+ 'index.js'
74
+ ].filter((candidate): candidate is string => Boolean(candidate));
75
+
76
+ for (let candidate of candidates) {
77
+ if (await pathExists(path.join(dirPath, candidate))) {
78
+ return candidate;
79
+ }
80
+ }
81
+
82
+ throw new Error(`Could not determine a default entry file for integration at ${dirPath}.`);
83
+ };
84
+
85
+ export let resolveIntegration = async (
86
+ input: string,
87
+ opts: { cwd?: string } = {}
88
+ ): Promise<ResolvedIntegration> => {
89
+ let cwd = opts.cwd ?? process.cwd();
90
+ let { rootDir, dirPath } = await resolveIntegrationDir(input, cwd);
91
+ let relativeDir = path.relative(rootDir, dirPath);
92
+
93
+ return {
94
+ input,
95
+ rootDir,
96
+ dirPath,
97
+ relativeDir: toPosixPath(relativeDir),
98
+ name: path.basename(dirPath),
99
+ entry: toPosixPath(path.join(relativeDir, await resolveDefaultEntry(dirPath)))
100
+ };
101
+ };
102
+
103
+ export let listWorkspaceIntegrations = async (opts: { cwd?: string } = {}) => {
104
+ let cwd = opts.cwd ?? process.cwd();
105
+ let rootDir = resolveSlatesCliRoot(cwd);
106
+ let integrationsDir = path.join(rootDir, 'integrations');
107
+
108
+ if (!(await pathExists(integrationsDir))) {
109
+ return [];
110
+ }
111
+
112
+ let entries = await readdir(integrationsDir, { withFileTypes: true });
113
+ let integrations = await Promise.all(
114
+ entries
115
+ .filter(entry => entry.isDirectory())
116
+ .map(async entry => {
117
+ let dirPath = path.join(integrationsDir, entry.name);
118
+ if (!(await pathExists(path.join(dirPath, 'package.json')))) {
119
+ return null;
120
+ }
121
+
122
+ return {
123
+ rootDir,
124
+ dirPath,
125
+ relativeDir: toPosixPath(path.relative(rootDir, dirPath)),
126
+ name: entry.name
127
+ } satisfies WorkspaceIntegrationSummary;
128
+ })
129
+ );
130
+
131
+ return integrations
132
+ .filter((integration): integration is WorkspaceIntegrationSummary => integration !== null)
133
+ .sort((a, b) => a.name.localeCompare(b.name));
134
+ };
@@ -0,0 +1,136 @@
1
+ import { checkbox } from '@inquirer/prompts';
2
+ import { execFile } from 'child_process';
3
+ import { randomUUID } from 'crypto';
4
+ import { createServer } from 'http';
5
+ import { promisify } from 'util';
6
+
7
+ let execFileAsync = promisify(execFile);
8
+ let DEFAULT_OAUTH_CALLBACK_PORT = 45873;
9
+
10
+ export let chooseScopes = async (
11
+ authMethod: any,
12
+ initialScopes: string[]
13
+ ): Promise<string[]> => {
14
+ let scopeIds = (authMethod.scopes ?? []).map((scope: any) => scope.id);
15
+ if (scopeIds.length === 0) {
16
+ return initialScopes;
17
+ }
18
+
19
+ return (await checkbox({
20
+ message: 'Choose OAuth scopes',
21
+ choices: authMethod.scopes.map((scope: any) => ({
22
+ name: `${scope.title} (${scope.id})`,
23
+ value: scope.id,
24
+ checked: initialScopes.length > 0 ? initialScopes.includes(scope.id) : true
25
+ }))
26
+ })) as string[];
27
+ };
28
+
29
+ export let openBrowser = async (url: string) => {
30
+ try {
31
+ if (process.platform === 'darwin') {
32
+ await execFileAsync('open', [url]);
33
+ return;
34
+ }
35
+
36
+ if (process.platform === 'win32') {
37
+ await execFileAsync('cmd', ['/c', 'start', '', url]);
38
+ return;
39
+ }
40
+
41
+ await execFileAsync('xdg-open', [url]);
42
+ } catch {
43
+ console.log(`Open this URL in your browser:\n${url}`);
44
+ }
45
+ };
46
+
47
+ export let createOAuthCallbackListener = async () => {
48
+ return new Promise<{
49
+ redirectUri: string;
50
+ state: string;
51
+ wait: () => Promise<{ code: string; state: string }>;
52
+ }>((resolve, reject) => {
53
+ let expectedState = randomUUID();
54
+ let settled = false;
55
+ let callbackPort = Number(process.env.SLATES_OAUTH_PORT ?? DEFAULT_OAUTH_CALLBACK_PORT);
56
+
57
+ let server = createServer((req, res) => {
58
+ try {
59
+ let url = new URL(req.url ?? '/', 'http://127.0.0.1');
60
+ let code = url.searchParams.get('code');
61
+ let state = url.searchParams.get('state');
62
+
63
+ if (!code || !state) {
64
+ res.statusCode = 400;
65
+ res.end('Missing code or state.');
66
+ return;
67
+ }
68
+
69
+ res.end('Authentication complete. You can close this window.');
70
+ server.close();
71
+ settled = true;
72
+ waiter.resolve({ code, state });
73
+ } catch (error) {
74
+ server.close();
75
+ settled = true;
76
+ waiter.reject(error);
77
+ }
78
+ });
79
+
80
+ let waiter = (() => {
81
+ let resolvePromise!: (value: { code: string; state: string }) => void;
82
+ let rejectPromise!: (error: unknown) => void;
83
+ let promise = new Promise<{ code: string; state: string }>((resolveFn, rejectFn) => {
84
+ resolvePromise = resolveFn;
85
+ rejectPromise = rejectFn;
86
+ });
87
+
88
+ return {
89
+ promise,
90
+ resolve: resolvePromise,
91
+ reject: rejectPromise
92
+ };
93
+ })();
94
+
95
+ let timeout = setTimeout(
96
+ () => {
97
+ if (settled) return;
98
+ server.close();
99
+ waiter.reject(new Error('Timed out waiting for the OAuth callback.'));
100
+ },
101
+ 5 * 60 * 1000
102
+ );
103
+
104
+ server.on('close', () => clearTimeout(timeout));
105
+ server.on('error', error => {
106
+ if (settled) return;
107
+ settled = true;
108
+
109
+ let errno = error as NodeJS.ErrnoException;
110
+ if (errno.code === 'EADDRINUSE') {
111
+ reject(
112
+ new Error(
113
+ `OAuth callback port ${callbackPort} is already in use. Free that port or set SLATES_OAUTH_PORT to a different fixed port.`
114
+ )
115
+ );
116
+ return;
117
+ }
118
+
119
+ reject(error);
120
+ });
121
+
122
+ server.listen(callbackPort, '127.0.0.1', () => {
123
+ let address = server.address();
124
+ if (!address || typeof address === 'string') {
125
+ reject(new Error('Could not determine OAuth callback address.'));
126
+ return;
127
+ }
128
+
129
+ resolve({
130
+ redirectUri: `http://127.0.0.1:${address.port}/callback`,
131
+ state: expectedState,
132
+ wait: () => waiter.promise
133
+ });
134
+ });
135
+ });
136
+ };
@@ -0,0 +1,159 @@
1
+ import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
2
+ import { JsonObject } from './types';
3
+
4
+ export let prettyJson = (value: unknown) => JSON.stringify(value, null, 2);
5
+
6
+ export let parseJsonObject = (value: string | undefined, label: string): JsonObject | null => {
7
+ if (!value) return null;
8
+
9
+ let parsed = JSON.parse(value);
10
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
11
+ throw new Error(`${label} must be a JSON object.`);
12
+ }
13
+
14
+ return parsed;
15
+ };
16
+
17
+ export let parseList = (value: string | undefined) =>
18
+ (value ?? '')
19
+ .split(',')
20
+ .map(item => item.trim())
21
+ .filter(Boolean);
22
+
23
+ export let print = (value: unknown) => {
24
+ console.log(prettyJson(value));
25
+ };
26
+
27
+ export let promptForString = async (d: {
28
+ message: string;
29
+ defaultValue?: string;
30
+ secret?: boolean;
31
+ }) => {
32
+ if (d.secret) {
33
+ return password({
34
+ message: d.message,
35
+ mask: '*'
36
+ });
37
+ }
38
+
39
+ return input({
40
+ message: d.message,
41
+ default: d.defaultValue
42
+ });
43
+ };
44
+
45
+ export let promptForSchemaValue = async (d: {
46
+ key: string;
47
+ schema: any;
48
+ currentValue?: any;
49
+ required?: boolean;
50
+ }): Promise<any> => {
51
+ let schema = d.schema ?? {};
52
+ let label = schema.title ?? d.key;
53
+
54
+ if (schema.enum && Array.isArray(schema.enum) && schema.enum.length > 0) {
55
+ return select({
56
+ message: label,
57
+ default: d.currentValue,
58
+ choices: schema.enum.map((value: string) => ({
59
+ name: value,
60
+ value
61
+ }))
62
+ });
63
+ }
64
+
65
+ if (schema.type === 'boolean') {
66
+ return confirm({
67
+ message: label,
68
+ default: d.currentValue ?? false
69
+ });
70
+ }
71
+
72
+ if (schema.type === 'array') {
73
+ if (Array.isArray(schema.items?.enum) && schema.items.enum.length > 0) {
74
+ return checkbox({
75
+ message: label,
76
+ choices: schema.items.enum.map((value: string) => ({
77
+ name: value,
78
+ value,
79
+ checked: Array.isArray(d.currentValue) ? d.currentValue.includes(value) : false
80
+ }))
81
+ });
82
+ }
83
+
84
+ let raw = await input({
85
+ message: `${label} (JSON array)`,
86
+ default: Array.isArray(d.currentValue) ? prettyJson(d.currentValue) : undefined
87
+ });
88
+
89
+ if (!raw.trim()) return [];
90
+ return JSON.parse(raw);
91
+ }
92
+
93
+ if (schema.type === 'number' || schema.type === 'integer') {
94
+ let raw = await input({
95
+ message: label,
96
+ default: d.currentValue !== undefined ? String(d.currentValue) : undefined
97
+ });
98
+
99
+ if (!raw.trim() && !d.required) return undefined;
100
+ return Number(raw);
101
+ }
102
+
103
+ if (schema.type === 'object') {
104
+ let raw = await input({
105
+ message: `${label} (JSON object)`,
106
+ default: d.currentValue ? prettyJson(d.currentValue) : '{}'
107
+ });
108
+
109
+ if (!raw.trim()) return {};
110
+ return JSON.parse(raw);
111
+ }
112
+
113
+ let isSecret = /secret|token|password/i.test(d.key);
114
+ let raw = await promptForString({
115
+ message: label,
116
+ defaultValue: typeof d.currentValue === 'string' ? d.currentValue : undefined,
117
+ secret: isSecret
118
+ });
119
+
120
+ if (!raw.trim() && !d.required) return undefined;
121
+ return raw;
122
+ };
123
+
124
+ export let promptForObjectSchema = async (schema: any, initialValue: JsonObject = {}) => {
125
+ let properties = schema?.properties ?? {};
126
+ let required = new Set<string>(schema?.required ?? []);
127
+ let keys = Object.keys(properties);
128
+
129
+ if (keys.length === 0) {
130
+ if (Object.keys(initialValue).length === 0) {
131
+ return {};
132
+ }
133
+
134
+ let raw = await input({
135
+ message: 'Enter JSON object',
136
+ default: Object.keys(initialValue).length > 0 ? prettyJson(initialValue) : '{}'
137
+ });
138
+
139
+ if (!raw.trim()) return {};
140
+ return parseJsonObject(raw, 'JSON input') ?? {};
141
+ }
142
+
143
+ let result: JsonObject = { ...initialValue };
144
+
145
+ for (let key of keys) {
146
+ let value = await promptForSchemaValue({
147
+ key,
148
+ schema: properties[key],
149
+ currentValue: initialValue[key],
150
+ required: required.has(key)
151
+ });
152
+
153
+ if (value !== undefined) {
154
+ result[key] = value;
155
+ }
156
+ }
157
+
158
+ return result;
159
+ };
@@ -0,0 +1,10 @@
1
+ export type JsonObject = Record<string, any>;
2
+
3
+ export type WithProfile = {
4
+ integration: string;
5
+ profile?: string;
6
+ };
7
+
8
+ export type JsonInput = {
9
+ input?: string;
10
+ };