directus-template-cli 0.7.0-beta.9 → 0.7.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.
- package/README.md +109 -8
- package/dist/commands/apply.d.ts +3 -2
- package/dist/commands/apply.js +80 -12
- package/dist/commands/base.d.ts +1 -0
- package/dist/commands/base.js +13 -0
- package/dist/commands/extract.d.ts +4 -2
- package/dist/commands/extract.js +46 -13
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +35 -22
- package/dist/lib/constants.d.ts +7 -1
- package/dist/lib/constants.js +7 -1
- package/dist/lib/init/index.js +34 -27
- package/dist/lib/load/apply-flags.d.ts +2 -1
- package/dist/lib/sdk.js +19 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/utils/auth.d.ts +4 -0
- package/dist/lib/utils/auth.js +34 -2
- package/dist/lib/utils/catch-error.d.ts +1 -1
- package/dist/lib/utils/catch-error.js +7 -0
- package/dist/lib/utils/parse-github-url.js +39 -10
- package/dist/services/docker.js +66 -9
- package/dist/services/execution-context.d.ts +18 -0
- package/dist/services/execution-context.js +20 -0
- package/dist/services/github.d.ts +7 -2
- package/dist/services/github.js +100 -10
- package/dist/services/posthog.d.ts +13 -0
- package/dist/services/posthog.js +27 -1
- package/oclif.manifest.json +21 -4
- package/package.json +1 -1
package/dist/lib/constants.js
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from 'chalk';
|
|
|
2
2
|
export const DIRECTUS_PURPLE = '#6644ff';
|
|
3
3
|
export const DIRECTUS_PINK = '#FF99DD';
|
|
4
4
|
export const SEPARATOR = '------------------';
|
|
5
|
+
export const pinkText = chalk.hex(DIRECTUS_PINK);
|
|
6
|
+
export const purpleText = chalk.hex(DIRECTUS_PURPLE);
|
|
5
7
|
export const COMMUNITY_TEMPLATE_REPO = {
|
|
6
8
|
string: 'github:directus-labs/directus-templates',
|
|
7
9
|
url: 'https://github.com/directus-labs/directus-templates',
|
|
@@ -17,4 +19,8 @@ export const POSTHOG_PUBLIC_KEY = 'phc_STopE6gj6LDIjYonVF7493kQJK8S4v0Xrl6YPr2z9
|
|
|
17
19
|
export const POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
18
20
|
export const DEFAULT_BRANCH = 'main';
|
|
19
21
|
export const BSL_LICENSE_URL = 'https://directus.io/bsl';
|
|
20
|
-
export const
|
|
22
|
+
export const BSL_EMAIL = 'licensing@directus.io';
|
|
23
|
+
export const BSL_LICENSE_HEADLINE = 'You REQUIRE a license to use Directus if your organization has more than $5MM USD a year in revenue and/or funding.';
|
|
24
|
+
export const BSL_LICENSE_TEXT = 'For all organizations with less than $5MM USD a year in revenue and funding, Directus is free for personal projects, hobby projects and in production. This second group does not require a license. Directus is licensed under BSL 1.1.';
|
|
25
|
+
export const BSL_LICENSE_CTA = `Visit ${pinkText(BSL_LICENSE_URL)} for more information or reach out to us at ${pinkText(BSL_EMAIL)}.`;
|
|
26
|
+
export const DEFAULT_DIRECTUS_URL = 'http://localhost:8055';
|
package/dist/lib/init/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { note, outro, spinner, log as clackLog } from '@clack/prompts';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
3
|
import { execa } from 'execa';
|
|
5
4
|
import { downloadTemplate } from 'giget';
|
|
6
5
|
import { glob } from 'glob';
|
|
@@ -14,12 +13,12 @@ import catchError from '../utils/catch-error.js';
|
|
|
14
13
|
import { createGigetString, parseGitHubUrl } from '../utils/parse-github-url.js';
|
|
15
14
|
import { readTemplateConfig } from '../utils/template-config.js';
|
|
16
15
|
import { DOCKER_CONFIG } from './config.js';
|
|
17
|
-
import { BSL_LICENSE_TEXT } from '../constants.js';
|
|
16
|
+
import { BSL_LICENSE_TEXT, BSL_LICENSE_HEADLINE, BSL_LICENSE_CTA, pinkText } from '../constants.js';
|
|
18
17
|
export async function init({ dir, flags }) {
|
|
19
18
|
// Check target directory
|
|
20
|
-
const shouldForce = flags.
|
|
19
|
+
const shouldForce = flags.overwriteDir;
|
|
21
20
|
if (fs.existsSync(dir) && !shouldForce) {
|
|
22
|
-
throw new Error('Directory already exists. Use --
|
|
21
|
+
throw new Error('Directory already exists. Use --overwrite-dir to override.');
|
|
23
22
|
}
|
|
24
23
|
// If template is a URL, we need to handle it differently
|
|
25
24
|
const isDirectUrl = flags.template?.startsWith('http');
|
|
@@ -104,36 +103,40 @@ export async function init({ dir, flags }) {
|
|
|
104
103
|
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
105
104
|
throw new Error(dockerStatus.message);
|
|
106
105
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
await dockerService.startContainers(directusDir);
|
|
107
|
+
const healthCheckUrl = `${directusInfo.url || 'http://localhost:8055'}${DOCKER_CONFIG.healthCheckEndpoint}`;
|
|
108
|
+
// Wait for healthy before proceeding
|
|
109
|
+
const isHealthy = await dockerService.waitForHealthy(healthCheckUrl);
|
|
110
|
+
if (!isHealthy) {
|
|
111
|
+
throw new Error('Directus failed to become healthy');
|
|
112
|
+
}
|
|
113
|
+
// Check if a template path is specified in the config and exists
|
|
114
|
+
let templatePath;
|
|
115
|
+
if (templateInfo?.config?.template && typeof templateInfo.config.template === 'string') {
|
|
116
|
+
templatePath = path.join(dir, templateInfo.config.template); // Path relative to root dir
|
|
117
|
+
}
|
|
118
|
+
if (templatePath && fs.existsSync(templatePath)) {
|
|
119
|
+
ux.stdout(`Applying template from: ${templatePath}`);
|
|
117
120
|
await ApplyCommand.run([
|
|
118
|
-
'
|
|
121
|
+
`--directusUrl=${directusInfo.url || 'http://localhost:8055'}`,
|
|
119
122
|
'-p',
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
`--userEmail=${directusInfo.email}`,
|
|
124
|
+
`--userPassword=${directusInfo.password}`,
|
|
122
125
|
`--templateLocation=${templatePath}`,
|
|
123
126
|
]);
|
|
124
127
|
}
|
|
125
|
-
|
|
126
|
-
ux.
|
|
127
|
-
throw error;
|
|
128
|
+
else {
|
|
129
|
+
ux.stdout('Skipping backend template application.');
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
// Detect package manager even if not installing dependencies
|
|
133
|
+
packageManager = await detectPackageManager(frontendDir);
|
|
130
134
|
// Install dependencies if requested
|
|
131
135
|
if (flags.installDeps) {
|
|
132
136
|
const s = spinner();
|
|
133
137
|
s.start('Installing dependencies');
|
|
134
138
|
try {
|
|
135
139
|
if (fs.existsSync(frontendDir)) {
|
|
136
|
-
packageManager = await detectPackageManager(frontendDir);
|
|
137
140
|
await installDependencies({
|
|
138
141
|
cwd: frontendDir,
|
|
139
142
|
packageManager,
|
|
@@ -156,14 +159,18 @@ export async function init({ dir, flags }) {
|
|
|
156
159
|
}
|
|
157
160
|
// Finishing up
|
|
158
161
|
const relativeDir = path.relative(process.cwd(), dir);
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
+
const directusUrl = directusInfo.url ?? 'http://localhost:8055';
|
|
163
|
+
const directusText = `- Directus is running on ${directusUrl}. \n`;
|
|
164
|
+
const directusLoginText = `- You can login with the email: ${pinkText(directusInfo.email)} and password: ${pinkText(directusInfo.password)}. \n`;
|
|
165
|
+
const frontendText = flags.frontend ? `- To start the frontend, run ${pinkText(`cd ${flags.frontend}`)} and then ${pinkText(`${packageManager?.name} run dev`)}. \n` : '';
|
|
166
|
+
const projectText = `- Navigate to your project directory using ${pinkText(`cd ${relativeDir}`)}. \n`;
|
|
162
167
|
const readmeText = '- Review the \`./README.md\` file for more information and next steps.';
|
|
163
|
-
const nextSteps =
|
|
168
|
+
const nextSteps = `${directusText}${directusLoginText}${projectText}${frontendText}${readmeText}`;
|
|
164
169
|
note(nextSteps, 'Next Steps');
|
|
165
|
-
clackLog.warn(
|
|
166
|
-
|
|
170
|
+
clackLog.warn(BSL_LICENSE_HEADLINE);
|
|
171
|
+
clackLog.info(BSL_LICENSE_TEXT);
|
|
172
|
+
clackLog.info(BSL_LICENSE_CTA);
|
|
173
|
+
outro(`Problems or questions? Hop into the community at ${pinkText('https://directus.chat')}`);
|
|
167
174
|
}
|
|
168
175
|
catch (error) {
|
|
169
176
|
catchError(error, {
|
|
@@ -15,7 +15,8 @@ export interface ApplyFlags {
|
|
|
15
15
|
templateType: 'community' | 'github' | 'local';
|
|
16
16
|
userEmail: string;
|
|
17
17
|
userPassword: string;
|
|
18
|
-
users
|
|
18
|
+
users?: boolean;
|
|
19
|
+
disableTelemetry?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare const loadFlags: readonly ["content", "dashboards", "extensions", "files", "flows", "permissions", "schema", "settings", "users"];
|
|
21
22
|
export declare function validateProgrammaticFlags(flags: ApplyFlags): ApplyFlags;
|
package/dist/lib/sdk.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { authentication, createDirectus, rest } from '@directus/sdk';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
3
|
import Bottleneck from 'bottleneck';
|
|
4
|
+
function log(message) {
|
|
5
|
+
ux.stdout(`${ux.colorize('dim', '--')} ${message}`);
|
|
6
|
+
}
|
|
4
7
|
export class DirectusError extends Error {
|
|
5
8
|
errors;
|
|
6
9
|
headers;
|
|
@@ -54,38 +57,44 @@ class Api {
|
|
|
54
57
|
retryCount: 3, // Retry a maximum of 3 times
|
|
55
58
|
});
|
|
56
59
|
this.limiter.on('failed', async (error, jobInfo) => {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
if (error instanceof TypeError && error.message === 'fetch failed' && error.cause?.code === 'ECONNREFUSED') {
|
|
62
|
+
log(`Connection refused. Please check the Directus URL and ensure the server is running. Not retrying. ${error.message}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
57
65
|
if (error instanceof DirectusError) {
|
|
58
66
|
const retryAfter = error.headers?.get('Retry-After');
|
|
59
67
|
const statusCode = error.status;
|
|
68
|
+
// If the status code is 400 or 401, we don't want to retry
|
|
69
|
+
if (statusCode === 400 || statusCode === 401) {
|
|
70
|
+
log(`Request failed with status ${statusCode}. Not retrying. ${error.message}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
60
73
|
if (statusCode === 429) {
|
|
61
74
|
const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 60_000;
|
|
62
|
-
|
|
75
|
+
log(`Rate limited. Retrying after ${delay}ms`);
|
|
63
76
|
return delay;
|
|
64
77
|
}
|
|
65
78
|
if (statusCode === 503) {
|
|
66
79
|
const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 5000;
|
|
67
|
-
|
|
80
|
+
log(`Server under pressure. Retrying after ${delay}ms`);
|
|
68
81
|
return delay;
|
|
69
82
|
}
|
|
70
|
-
// If the status code is 400 or 401, we don't want to retry
|
|
71
|
-
if (statusCode === 400 || statusCode === 401) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
83
|
}
|
|
75
84
|
// For other errors, use exponential backoff, but only if we haven't exceeded retryCount
|
|
76
85
|
if (jobInfo.retryCount < 3) {
|
|
77
86
|
const delay = Math.min(1000 * 2 ** jobInfo.retryCount, 30_000);
|
|
78
|
-
|
|
87
|
+
log(`Request failed. Retrying after ${delay}ms`);
|
|
79
88
|
return delay;
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
log('Max retries reached, not retrying further');
|
|
82
91
|
});
|
|
83
92
|
this.limiter.on('retry', (error, jobInfo) => {
|
|
84
|
-
|
|
93
|
+
log(`Retrying job (attempt ${jobInfo.retryCount + 1})`);
|
|
85
94
|
});
|
|
86
95
|
this.limiter.on('depleted', empty => {
|
|
87
96
|
if (empty) {
|
|
88
|
-
|
|
97
|
+
log('Rate limit quota depleted. Requests will be queued.');
|
|
89
98
|
}
|
|
90
99
|
});
|
|
91
100
|
}
|
package/dist/lib/types.d.ts
CHANGED
package/dist/lib/utils/auth.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export declare function getDirectusUrl(): Promise<string>;
|
|
|
15
15
|
* @returns The Directus token
|
|
16
16
|
*/
|
|
17
17
|
export declare function getDirectusToken(directusUrl: string): Promise<string>;
|
|
18
|
+
export declare function getDirectusEmailAndPassword(): Promise<{
|
|
19
|
+
userEmail: string;
|
|
20
|
+
userPassword: string;
|
|
21
|
+
}>;
|
|
18
22
|
/**
|
|
19
23
|
* Initialize the Directus API with the provided flags and log in the user
|
|
20
24
|
* @param flags - The validated ApplyFlags
|
package/dist/lib/utils/auth.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { readMe } from '@directus/sdk';
|
|
2
|
-
import { text, log, isCancel } from '@clack/prompts';
|
|
2
|
+
import { text, log, isCancel, password } from '@clack/prompts';
|
|
3
3
|
import { ux } from '@oclif/core';
|
|
4
4
|
import { api } from '../sdk.js';
|
|
5
5
|
import catchError from './catch-error.js';
|
|
6
6
|
import validateUrl from './validate-url.js';
|
|
7
|
+
import { DEFAULT_DIRECTUS_URL } from '../../lib/constants.js';
|
|
7
8
|
/**
|
|
8
9
|
* Get the Directus URL from the user
|
|
9
10
|
* @returns The Directus URL
|
|
10
11
|
*/
|
|
11
12
|
export async function getDirectusUrl() {
|
|
12
13
|
const directusUrl = await text({
|
|
13
|
-
placeholder:
|
|
14
|
+
placeholder: DEFAULT_DIRECTUS_URL,
|
|
14
15
|
message: 'What is your Directus URL?',
|
|
15
16
|
});
|
|
16
17
|
if (isCancel(directusUrl)) {
|
|
17
18
|
log.info('Exiting...');
|
|
18
19
|
ux.exit(0);
|
|
19
20
|
}
|
|
21
|
+
if (!directusUrl) {
|
|
22
|
+
ux.warn(`No URL provided, using default: ${DEFAULT_DIRECTUS_URL}`);
|
|
23
|
+
return DEFAULT_DIRECTUS_URL;
|
|
24
|
+
}
|
|
20
25
|
// Validate URL
|
|
21
26
|
if (!validateUrl(directusUrl)) {
|
|
22
27
|
ux.warn('Invalid URL');
|
|
@@ -56,6 +61,33 @@ export async function getDirectusToken(directusUrl) {
|
|
|
56
61
|
return getDirectusToken(directusUrl);
|
|
57
62
|
}
|
|
58
63
|
}
|
|
64
|
+
export async function getDirectusEmailAndPassword() {
|
|
65
|
+
const userEmail = await text({
|
|
66
|
+
message: 'What is your email?',
|
|
67
|
+
validate(value) {
|
|
68
|
+
if (!value) {
|
|
69
|
+
return 'Email is required';
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (isCancel(userEmail)) {
|
|
74
|
+
log.info('Exiting...');
|
|
75
|
+
ux.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const userPassword = await password({
|
|
78
|
+
message: 'What is your password?',
|
|
79
|
+
validate(value) {
|
|
80
|
+
if (!value) {
|
|
81
|
+
return 'Password is required';
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (isCancel(userPassword)) {
|
|
86
|
+
log.info('Exiting...');
|
|
87
|
+
ux.exit(0);
|
|
88
|
+
}
|
|
89
|
+
return { userEmail, userPassword };
|
|
90
|
+
}
|
|
59
91
|
/**
|
|
60
92
|
* Initialize the Directus API with the provided flags and log in the user
|
|
61
93
|
* @param flags - The validated ApplyFlags
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
interface ErrorHandlerOptions {
|
|
5
5
|
/** Additional context to be included in the error log. */
|
|
6
|
-
context?: Record<string,
|
|
6
|
+
context?: Record<string, unknown>;
|
|
7
7
|
/** If true, the error will be treated as fatal and the process will exit. */
|
|
8
8
|
fatal?: boolean;
|
|
9
9
|
/** If true, the error will be logged to a file. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
2
|
import { DirectusError } from '../sdk.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { captureException } from '../../services/posthog.js';
|
|
5
|
+
import { getExecutionContext } from '../../services/execution-context.js';
|
|
4
6
|
/**
|
|
5
7
|
* Handles errors by formatting them and optionally logging to console and file.
|
|
6
8
|
* @param error - The error to be handled.
|
|
@@ -9,6 +11,7 @@ import { logger } from '../utils/logger.js';
|
|
|
9
11
|
*/
|
|
10
12
|
export default function catchError(error, options = {}) {
|
|
11
13
|
const { context = {}, fatal = false, logToFile = true } = options;
|
|
14
|
+
const { distinctId, disableTelemetry } = getExecutionContext();
|
|
12
15
|
let errorMessage;
|
|
13
16
|
if (error instanceof DirectusError) {
|
|
14
17
|
errorMessage = error.message;
|
|
@@ -19,6 +22,10 @@ export default function catchError(error, options = {}) {
|
|
|
19
22
|
else {
|
|
20
23
|
errorMessage = `Unknown error: ${JSON.stringify(error)}`;
|
|
21
24
|
}
|
|
25
|
+
// Capture exception before logging/exiting
|
|
26
|
+
if (!disableTelemetry && distinctId) {
|
|
27
|
+
captureException({ error, distinctId, properties: { context } });
|
|
28
|
+
}
|
|
22
29
|
// Format the error message with context if provided
|
|
23
30
|
const formattedMessage = [
|
|
24
31
|
errorMessage,
|
|
@@ -39,30 +39,59 @@ export function parseGitHubUrl(url) {
|
|
|
39
39
|
if (!url) {
|
|
40
40
|
throw new Error('URL is required');
|
|
41
41
|
}
|
|
42
|
-
// Clean the URL first
|
|
43
42
|
const cleanedUrl = cleanGitHubUrl(url);
|
|
44
|
-
// Handle full GitHub URLs
|
|
45
43
|
if (cleanedUrl.includes('github.com')) {
|
|
46
44
|
try {
|
|
47
45
|
const parsed = new URL(cleanedUrl);
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
throw new Error('Invalid GitHub URL format');
|
|
46
|
+
const pathParts = parsed.pathname.split('/').filter(Boolean);
|
|
47
|
+
if (pathParts.length < 2) {
|
|
48
|
+
throw new Error('Invalid GitHub URL format: Needs owner and repo.');
|
|
51
49
|
}
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
50
|
+
const owner = pathParts[0];
|
|
51
|
+
const repo = pathParts[1];
|
|
52
|
+
let ref = DEFAULT_BRANCH; // Default ref
|
|
53
|
+
let path;
|
|
54
|
+
// Check for /tree/ref/ or /blob/ref/ patterns
|
|
55
|
+
const treeIndex = pathParts.indexOf('tree');
|
|
56
|
+
const blobIndex = pathParts.indexOf('blob');
|
|
57
|
+
let refIndex = -1;
|
|
58
|
+
if (treeIndex > 1 && treeIndex + 1 < pathParts.length) {
|
|
59
|
+
refIndex = treeIndex + 1;
|
|
60
|
+
}
|
|
61
|
+
else if (blobIndex > 1 && blobIndex + 1 < pathParts.length) {
|
|
62
|
+
refIndex = blobIndex + 1;
|
|
63
|
+
}
|
|
64
|
+
if (refIndex !== -1) {
|
|
65
|
+
ref = pathParts[refIndex];
|
|
66
|
+
// Path is everything after the ref
|
|
67
|
+
path = pathParts.slice(refIndex + 1).join('/') || undefined;
|
|
68
|
+
}
|
|
69
|
+
else if (pathParts.length > 2) {
|
|
70
|
+
// If no tree/blob, but more parts exist, assume it's part of the path
|
|
71
|
+
// This handles cases like github.com/owner/repo/some/path without a specific ref marker
|
|
72
|
+
path = pathParts.slice(2).join('/') || undefined;
|
|
73
|
+
// If URL has an explicit ?ref= param, use that, otherwise keep default
|
|
74
|
+
ref = parsed.searchParams.get('ref') || ref;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// No path, just owner/repo
|
|
78
|
+
ref = parsed.searchParams.get('ref') || ref;
|
|
79
|
+
}
|
|
80
|
+
// Ensure path is undefined if empty string
|
|
81
|
+
if (path === '')
|
|
82
|
+
path = undefined;
|
|
55
83
|
return { owner, repo, path, ref };
|
|
56
84
|
}
|
|
57
85
|
catch (error) {
|
|
58
|
-
throw new Error(`Invalid GitHub URL: ${url}`);
|
|
86
|
+
throw new Error(`Invalid GitHub URL: ${url}. Error: ${error.message}`);
|
|
59
87
|
}
|
|
60
88
|
}
|
|
61
|
-
// Handle repository paths (owner/repo format)
|
|
89
|
+
// Handle repository paths (owner/repo/path format) without github.com
|
|
62
90
|
const parts = cleanedUrl.split('/').filter(Boolean);
|
|
63
91
|
if (parts.length >= 2) {
|
|
64
92
|
const [owner, repo, ...rest] = parts;
|
|
65
93
|
const path = rest.length > 0 ? rest.join('/') : undefined;
|
|
94
|
+
// Assume default branch for simple paths unless we add ref detection here too
|
|
66
95
|
return { owner, repo, path, ref: DEFAULT_BRANCH };
|
|
67
96
|
}
|
|
68
97
|
// Handle simple template names using DEFAULT_REPO
|
package/dist/services/docker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spinner } from '@clack/prompts';
|
|
1
|
+
import { spinner, log } from '@clack/prompts';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { ux } from '@oclif/core';
|
|
@@ -89,23 +89,80 @@ async function checkDocker() {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* Get the list of image names defined in the docker-compose file
|
|
93
|
+
* @param {string} cwd - The current working directory
|
|
94
|
+
* @returns {Promise<string[]>} - A list of image names
|
|
95
|
+
*/
|
|
96
|
+
async function getRequiredImagesFromCompose(cwd) {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await execa('docker', ['compose', 'config', '--images'], { cwd });
|
|
99
|
+
// stdout contains a list of image names, one per line
|
|
100
|
+
return stdout.split('\n').filter(img => img.trim() !== ''); // Filter out empty lines
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Handle potential errors, e.g., compose file not found or invalid
|
|
104
|
+
log.error('Failed to get images from docker-compose file.');
|
|
105
|
+
catchError(error, {
|
|
106
|
+
context: { cwd, function: 'getRequiredImagesFromCompose' },
|
|
107
|
+
fatal: false, // Don't necessarily exit, maybe let startContainers handle it
|
|
108
|
+
logToFile: true,
|
|
109
|
+
});
|
|
110
|
+
return []; // Return empty list on error
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if a list of Docker images exist locally
|
|
115
|
+
* @param {string[]} imageNames - An array of Docker image names (e.g., "postgres:16")
|
|
116
|
+
* @returns {Promise<boolean>} - True if all images exist locally, false otherwise
|
|
117
|
+
*/
|
|
118
|
+
async function checkImagesExist(imageNames) {
|
|
119
|
+
if (imageNames.length === 0) {
|
|
120
|
+
return true; // No images to check, technically they all "exist"
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
// Use Promise.allSettled to check all images even if some commands fail
|
|
124
|
+
const results = await Promise.allSettled(imageNames.map(imageName => execa('docker', ['inspect', '--type=image', imageName])));
|
|
125
|
+
// Check if all inspect commands succeeded (exit code 0)
|
|
126
|
+
return results.every(result => result.status === 'fulfilled' && result.value.exitCode === 0);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// This catch block might be redundant due to allSettled, but good for safety
|
|
130
|
+
log.error('Error checking for Docker images.');
|
|
131
|
+
catchError(error, {
|
|
132
|
+
context: { imageNames, function: 'checkImagesExist' },
|
|
133
|
+
fatal: false,
|
|
134
|
+
logToFile: true,
|
|
135
|
+
});
|
|
136
|
+
return false; // Assume images don't exist if there's an error checking
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Start Docker containers using docker compose
|
|
93
141
|
* @param {string} cwd - The current working directory
|
|
94
142
|
* @returns {Promise<void>} - Returns nothing
|
|
95
143
|
*/
|
|
96
144
|
async function startContainers(cwd) {
|
|
145
|
+
const s = spinner();
|
|
97
146
|
try {
|
|
98
147
|
// Check if required ports are available
|
|
99
148
|
await checkRequiredPorts();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
// Get required images from compose file
|
|
150
|
+
const requiredImages = await getRequiredImagesFromCompose(cwd);
|
|
151
|
+
const imagesExist = await checkImagesExist(requiredImages);
|
|
152
|
+
// Log a message if images need downloading
|
|
153
|
+
if (!imagesExist && requiredImages.length > 0) {
|
|
154
|
+
log.info('Required Docker image(s) are missing and will be downloaded.');
|
|
155
|
+
}
|
|
156
|
+
const startMessage = imagesExist || requiredImages.length === 0 ? 'Starting Docker containers...' : 'Downloading required Docker images...';
|
|
157
|
+
const endMessage = imagesExist || requiredImages.length === 0 ? 'Docker containers running!' : 'Docker images downloaded and containers started!';
|
|
158
|
+
s.start(startMessage); // Start spinner with the appropriate message
|
|
159
|
+
await execa('docker', ['compose', 'up', '-d'], {
|
|
103
160
|
cwd,
|
|
104
|
-
}).then(() => {
|
|
105
|
-
s.stop('Docker containers running!');
|
|
106
161
|
});
|
|
162
|
+
s.stop(endMessage); // Update spinner message on success
|
|
107
163
|
}
|
|
108
164
|
catch (error) {
|
|
165
|
+
s.stop('Error starting Docker containers.'); // Stop spinner on error
|
|
109
166
|
catchError(error, {
|
|
110
167
|
context: { cwd, function: 'startContainers' },
|
|
111
168
|
fatal: true,
|
|
@@ -115,13 +172,13 @@ async function startContainers(cwd) {
|
|
|
115
172
|
}
|
|
116
173
|
}
|
|
117
174
|
/**
|
|
118
|
-
* Stop Docker containers
|
|
175
|
+
* Stop Docker containers using docker compose
|
|
119
176
|
* @param {string} cwd - The current working directory
|
|
120
177
|
* @returns {Promise<void>} - Returns nothing
|
|
121
178
|
*/
|
|
122
179
|
async function stopContainers(cwd) {
|
|
123
180
|
try {
|
|
124
|
-
return execa('docker
|
|
181
|
+
return execa('docker', ['compose', 'down'], {
|
|
125
182
|
cwd,
|
|
126
183
|
}).then(() => { });
|
|
127
184
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the structure for the execution context, holding telemetry information.
|
|
3
|
+
*/
|
|
4
|
+
export interface ExecutionContext {
|
|
5
|
+
distinctId?: string;
|
|
6
|
+
disableTelemetry?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sets the global execution context.
|
|
10
|
+
* This should be called early in the command lifecycle.
|
|
11
|
+
* @param context The context object containing distinctId and disableTelemetry status.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setExecutionContext(context: ExecutionContext): void;
|
|
14
|
+
/**
|
|
15
|
+
* Gets the currently set global execution context.
|
|
16
|
+
* @returns The current execution context.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getExecutionContext(): ExecutionContext;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Module-level variable to hold the current context.
|
|
2
|
+
// Initialize with default values (telemetry enabled, no distinctId yet).
|
|
3
|
+
let currentContext = {
|
|
4
|
+
disableTelemetry: false,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Sets the global execution context.
|
|
8
|
+
* This should be called early in the command lifecycle.
|
|
9
|
+
* @param context The context object containing distinctId and disableTelemetry status.
|
|
10
|
+
*/
|
|
11
|
+
export function setExecutionContext(context) {
|
|
12
|
+
currentContext = context;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Gets the currently set global execution context.
|
|
16
|
+
* @returns The current execution context.
|
|
17
|
+
*/
|
|
18
|
+
export function getExecutionContext() {
|
|
19
|
+
return currentContext;
|
|
20
|
+
}
|
|
@@ -5,14 +5,19 @@ interface GitHubUrlParts {
|
|
|
5
5
|
ref?: string;
|
|
6
6
|
repo: string;
|
|
7
7
|
}
|
|
8
|
+
export interface TemplateInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
8
13
|
export interface GitHubService {
|
|
9
14
|
getTemplateDirectories(template: string, customUrl?: string): Promise<string[]>;
|
|
10
|
-
getTemplates(customUrl?: string): Promise<
|
|
15
|
+
getTemplates(customUrl?: string): Promise<TemplateInfo[]>;
|
|
11
16
|
parseGitHubUrl(url: string): GitHubUrlParts;
|
|
12
17
|
}
|
|
13
18
|
export declare function createGitHub(token?: string): {
|
|
14
19
|
getTemplateDirectories: (template: string, customUrl?: string) => Promise<string[]>;
|
|
15
|
-
getTemplates: (customUrl?: string) => Promise<
|
|
20
|
+
getTemplates: (customUrl?: string) => Promise<TemplateInfo[]>;
|
|
16
21
|
parseGitHubUrl: typeof parseGitHubUrl;
|
|
17
22
|
};
|
|
18
23
|
export {};
|