@temporalio/create 0.22.0 → 1.0.0-rc.1
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 +2 -2
- package/package.json +21 -19
- package/src/create-project.ts +220 -0
- package/src/helpers/fetch-samples.ts +25 -0
- package/src/helpers/get-error-code.ts +11 -0
- package/src/helpers/git.ts +79 -0
- package/src/helpers/install.ts +62 -0
- package/src/helpers/is-online.ts +44 -0
- package/src/helpers/is-writeable.ts +17 -0
- package/src/helpers/make-dir.ts +6 -0
- package/src/helpers/samples.ts +136 -0
- package/src/helpers/strip-snip-comments.ts +14 -0
- package/src/helpers/subprocess.ts +38 -0
- package/src/helpers/validate-pkg.ts +17 -0
- package/src/index.ts +217 -0
- package/src/pkg.ts +6 -0
package/README.md
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@temporalio/create)
|
|
4
4
|
|
|
5
|
-
Project initializer for [Temporal](https://temporal.io)'s [TypeScript SDK](https://docs.temporal.io/
|
|
5
|
+
Project initializer for [Temporal](https://temporal.io)'s [TypeScript SDK](https://docs.temporal.io/typescript/introduction/).
|
|
6
6
|
|
|
7
|
-
[`@temporalio/create` documentation](https://docs.temporal.io/
|
|
7
|
+
[`@temporalio/create` documentation](https://docs.temporal.io/typescript/package-initializer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@temporalio/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"description": "Create a Temporal project from template",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": "cli.js",
|
|
@@ -14,33 +14,30 @@
|
|
|
14
14
|
"init",
|
|
15
15
|
"workflow"
|
|
16
16
|
],
|
|
17
|
-
"files": [
|
|
18
|
-
"lib"
|
|
19
|
-
],
|
|
20
17
|
"author": "Roey Berman <roey@temporal.io>",
|
|
21
18
|
"license": "MIT",
|
|
22
19
|
"dependencies": {
|
|
23
20
|
"async-retry": "^1.3.3",
|
|
24
|
-
"chalk": "^
|
|
25
|
-
"chalk-template": "^0.
|
|
26
|
-
"commander": "^
|
|
21
|
+
"chalk": "^5.0.1",
|
|
22
|
+
"chalk-template": "^0.4.0",
|
|
23
|
+
"commander": "^9.3.0",
|
|
27
24
|
"dedent": "^0.7.0",
|
|
28
|
-
"glob": "^
|
|
29
|
-
"got": "^
|
|
30
|
-
"prompts": "^2.4.
|
|
31
|
-
"tar": "^
|
|
25
|
+
"glob": "^8.0.3",
|
|
26
|
+
"got": "^12.1.0",
|
|
27
|
+
"prompts": "^2.4.2",
|
|
28
|
+
"tar": "^6.1.11",
|
|
32
29
|
"update-check": "1.5.4",
|
|
33
|
-
"validate-npm-package-name": "^
|
|
30
|
+
"validate-npm-package-name": "^4.0.0"
|
|
34
31
|
},
|
|
35
32
|
"devDependencies": {
|
|
36
|
-
"@types/async-retry": "^1.4.
|
|
33
|
+
"@types/async-retry": "^1.4.4",
|
|
37
34
|
"@types/cross-spawn": "^6.0.2",
|
|
38
35
|
"@types/got": "^9.6.12",
|
|
39
|
-
"@types/node": "^
|
|
40
|
-
"@types/prompts": "2.0.
|
|
41
|
-
"@types/rimraf": "^3.0.
|
|
42
|
-
"@types/tar": "^
|
|
43
|
-
"@types/validate-npm-package-name": "^
|
|
36
|
+
"@types/node": "^18.0.0",
|
|
37
|
+
"@types/prompts": "2.0.14",
|
|
38
|
+
"@types/rimraf": "^3.0.2",
|
|
39
|
+
"@types/tar": "^6.1.1",
|
|
40
|
+
"@types/validate-npm-package-name": "^4.0.0"
|
|
44
41
|
},
|
|
45
42
|
"publishConfig": {
|
|
46
43
|
"access": "public"
|
|
@@ -48,5 +45,10 @@
|
|
|
48
45
|
"engines": {
|
|
49
46
|
"node": ">=14.0.0"
|
|
50
47
|
},
|
|
51
|
-
"
|
|
48
|
+
"files": [
|
|
49
|
+
"cli.js",
|
|
50
|
+
"src",
|
|
51
|
+
"lib"
|
|
52
|
+
],
|
|
53
|
+
"gitHead": "723de0fbc7a04e68084ec99453578e7027eb3803"
|
|
52
54
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/create-app.ts
|
|
2
|
+
import retry from 'async-retry';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import chalkTemplate from 'chalk-template';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import { access, rm, readFile } from 'fs/promises';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
downloadAndExtractSample,
|
|
11
|
+
downloadAndExtractRepo,
|
|
12
|
+
getRepoInfo,
|
|
13
|
+
hasSample,
|
|
14
|
+
checkForPackageJson,
|
|
15
|
+
RepoInfo,
|
|
16
|
+
} from './helpers/samples.js';
|
|
17
|
+
import { makeDir } from './helpers/make-dir.js';
|
|
18
|
+
import { tryGitInit } from './helpers/git.js';
|
|
19
|
+
import { install, updateNodeVersion, replaceTemporalVersion } from './helpers/install.js';
|
|
20
|
+
import { testIfThisComputerIsOnline } from './helpers/is-online.js';
|
|
21
|
+
import { isWriteable } from './helpers/is-writeable.js';
|
|
22
|
+
import { getErrorCode } from './helpers/get-error-code.js';
|
|
23
|
+
import { stripSnipComments } from './helpers/strip-snip-comments.js';
|
|
24
|
+
import { fetchSamples } from './helpers/fetch-samples.js';
|
|
25
|
+
|
|
26
|
+
export class DownloadError extends Error {}
|
|
27
|
+
|
|
28
|
+
export async function createApp({
|
|
29
|
+
appPath,
|
|
30
|
+
useYarn,
|
|
31
|
+
gitInit,
|
|
32
|
+
temporalioVersion,
|
|
33
|
+
sample,
|
|
34
|
+
samplePath,
|
|
35
|
+
}: {
|
|
36
|
+
appPath: string;
|
|
37
|
+
useYarn: boolean;
|
|
38
|
+
gitInit?: boolean;
|
|
39
|
+
temporalioVersion?: string;
|
|
40
|
+
sample: string;
|
|
41
|
+
samplePath?: string;
|
|
42
|
+
}): Promise<void> {
|
|
43
|
+
let repoInfo: RepoInfo | undefined;
|
|
44
|
+
let repoUrl: URL | undefined;
|
|
45
|
+
|
|
46
|
+
const isOnline = await testIfThisComputerIsOnline();
|
|
47
|
+
if (!isOnline) {
|
|
48
|
+
console.error(`Unable to reach ${chalk.bold(`github.com`)}. Perhaps you are not connected to the internet?`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
repoUrl = new URL(sample);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (getErrorCode(error) !== 'ERR_INVALID_URL') {
|
|
56
|
+
console.error(error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (repoUrl) {
|
|
62
|
+
if (repoUrl.origin !== 'https://github.com') {
|
|
63
|
+
console.error(
|
|
64
|
+
`Invalid URL: ${chalk.red(
|
|
65
|
+
`"${sample}"`
|
|
66
|
+
)}. Only GitHub repositories are supported. Please use a GitHub URL and try again.`
|
|
67
|
+
);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
repoInfo = await getRepoInfo(repoUrl, samplePath);
|
|
73
|
+
await checkForPackageJson(repoInfo);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(e);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
} else if (sample !== '__internal-testing-retry') {
|
|
79
|
+
const found = await hasSample(sample);
|
|
80
|
+
|
|
81
|
+
if (!found) {
|
|
82
|
+
console.error(
|
|
83
|
+
`Could not locate a sample named ${chalk.red(`"${sample}"`)}. It could be due to the following:\n`,
|
|
84
|
+
`1. Your spelling of sample ${chalk.red(`"${sample}"`)} might be incorrect.\n`,
|
|
85
|
+
`2. You might not be connected to the internet.\n`
|
|
86
|
+
);
|
|
87
|
+
const samples = await fetchSamples();
|
|
88
|
+
console.error(`Available samples:\n\n${samples.join('\n')}\n`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const root = path.resolve(appPath);
|
|
94
|
+
|
|
95
|
+
if (!(await isWriteable(path.dirname(root)))) {
|
|
96
|
+
console.error('The application path is not writable, please check folder permissions and try again.');
|
|
97
|
+
console.error('It is likely you do not have write permissions for this folder.');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const appName = path.basename(root);
|
|
102
|
+
|
|
103
|
+
console.log(`Creating a new Temporal project in ${chalk.green(root)}/`);
|
|
104
|
+
console.log();
|
|
105
|
+
|
|
106
|
+
let directoryExists = true;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await access(root);
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
const code = getErrorCode(error);
|
|
112
|
+
|
|
113
|
+
if (code === 'ENOENT') {
|
|
114
|
+
directoryExists = false;
|
|
115
|
+
} else if (code === 'EACCES') {
|
|
116
|
+
console.error(`Unable to access directory ${chalk.bold(root + '/')} (Error: permission denied)`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
} else {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (directoryExists) {
|
|
124
|
+
const res = await prompts({
|
|
125
|
+
type: 'confirm',
|
|
126
|
+
name: 'shouldReplace',
|
|
127
|
+
message: `Directory ${chalk.green(root + '/')} already exists. Would you like to replace it?`,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!res.shouldReplace) {
|
|
131
|
+
console.error('Exiting. You can re-run this command with a different project name.');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await rm(root, { recursive: true, force: true, maxRetries: 5 });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await makeDir(root);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (getErrorCode(error) === 'EACCES') {
|
|
142
|
+
console.error(`Unable to cd into directory ${chalk.bold(root + '/')} (Error: permission denied)`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
} else {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* If a sample repository is provided, clone it.
|
|
151
|
+
*/
|
|
152
|
+
try {
|
|
153
|
+
if (repoInfo) {
|
|
154
|
+
const repoInfo2 = repoInfo;
|
|
155
|
+
console.log(`Downloading files from repo ${chalk.cyan(sample)}. This might take a moment.`);
|
|
156
|
+
console.log();
|
|
157
|
+
await retry(() => downloadAndExtractRepo(root, repoInfo2), {
|
|
158
|
+
retries: 3,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`Downloading files for sample ${chalk.cyan(sample)}. This might take a moment.`);
|
|
162
|
+
console.log();
|
|
163
|
+
await retry(() => downloadAndExtractSample(root, sample), {
|
|
164
|
+
retries: 3,
|
|
165
|
+
});
|
|
166
|
+
await stripSnipComments(root);
|
|
167
|
+
}
|
|
168
|
+
} catch (reason) {
|
|
169
|
+
let message = 'Unable to download';
|
|
170
|
+
if (reason instanceof Error) {
|
|
171
|
+
message = reason.message;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new DownloadError(message);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('Installing packages. This might take a couple of minutes.');
|
|
178
|
+
console.log();
|
|
179
|
+
|
|
180
|
+
await updateNodeVersion({ root });
|
|
181
|
+
if (temporalioVersion) {
|
|
182
|
+
await replaceTemporalVersion({ root, useYarn, temporalioVersion });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await install({ root, useYarn });
|
|
186
|
+
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
if (await tryGitInit(root, gitInit)) {
|
|
190
|
+
console.log('Initialized a git repository.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const messageFile = path.join(root, '.post-create');
|
|
194
|
+
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(`${chalk.green('Success!')} Created project ${chalk.bold(appName)} at:`);
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.bold(appPath + '/'));
|
|
199
|
+
console.log();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await access(messageFile);
|
|
203
|
+
const message = await readFile(messageFile, 'utf8');
|
|
204
|
+
|
|
205
|
+
// Hack for creating a TemplateStringsArray
|
|
206
|
+
// Required by chalk-template
|
|
207
|
+
class MockTemplateString extends Array implements TemplateStringsArray {
|
|
208
|
+
constructor(public readonly raw: string[]) {
|
|
209
|
+
super();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
console.log(chalkTemplate(new MockTemplateString([message])));
|
|
213
|
+
await rm(messageFile);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const code = getErrorCode(error);
|
|
216
|
+
if (code !== 'ENOENT') {
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
|
|
3
|
+
const SAMPLE_REPO_CONTENTS = 'https://api.github.com/repos/temporalio/samples-typescript/contents/';
|
|
4
|
+
|
|
5
|
+
interface File {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function fetchSamples(): Promise<string[]> {
|
|
11
|
+
let response;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// https://github.com/sindresorhus/got/blob/main/documentation/3-streams.md#response-1
|
|
15
|
+
response = await got(SAMPLE_REPO_CONTENTS);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`Unable to reach github.com`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = JSON.parse(response.body) as File[];
|
|
21
|
+
|
|
22
|
+
return files
|
|
23
|
+
.filter((file) => file.type === 'dir' && file.name !== 'test' && !file.name.startsWith('.'))
|
|
24
|
+
.map(({ name }) => name);
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ErrorWithCode {
|
|
2
|
+
code: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getErrorCode(error: unknown): string {
|
|
6
|
+
if ((error as ErrorWithCode).code !== undefined && typeof (error as ErrorWithCode).code === 'string') {
|
|
7
|
+
return (error as ErrorWithCode).code;
|
|
8
|
+
} else {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* eslint-disable no-empty */
|
|
2
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/git.ts
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { rm } from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
|
|
8
|
+
const NOT_A_GIT_REPOSITORY_STATUS_CODE = 128;
|
|
9
|
+
|
|
10
|
+
function isInGitRepository(): boolean {
|
|
11
|
+
try {
|
|
12
|
+
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error: any) {
|
|
15
|
+
if (error.status === NOT_A_GIT_REPOSITORY_STATUS_CODE) {
|
|
16
|
+
return false;
|
|
17
|
+
} else {
|
|
18
|
+
// Unknown error. To be safe, assume we're in a repo.
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HG_ERROR_STATUS_CODE = 255;
|
|
25
|
+
|
|
26
|
+
function isInMercurialRepository(): boolean {
|
|
27
|
+
try {
|
|
28
|
+
execSync('hg --cwd . root', { stdio: 'ignore' });
|
|
29
|
+
return true;
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
// There isn't anything more specific about `error` that we can pattern match against
|
|
32
|
+
if (error.status === HG_ERROR_STATUS_CODE && /Command failed/.test(error.message)) {
|
|
33
|
+
return false;
|
|
34
|
+
} else {
|
|
35
|
+
// Unknown error. To be safe, assume we're in a repo.
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function tryGitInit(root: string, useGit?: boolean): Promise<boolean> {
|
|
42
|
+
if (useGit === false) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
let didInit = false;
|
|
46
|
+
const exec = (command: string) => execSync(command, { stdio: 'ignore', cwd: root });
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// If user didn't include --use-git, and they might be in an existing repo,
|
|
50
|
+
// ask before `git init`ing
|
|
51
|
+
if (useGit === undefined && (isInGitRepository() || isInMercurialRepository())) {
|
|
52
|
+
const res = await prompts({
|
|
53
|
+
type: 'confirm',
|
|
54
|
+
name: 'shouldInit',
|
|
55
|
+
message: `Would you like me to initialize a git repository for the project?`,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!res.shouldInit) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exec('git init');
|
|
64
|
+
didInit = true;
|
|
65
|
+
|
|
66
|
+
exec('git checkout -b main');
|
|
67
|
+
|
|
68
|
+
exec('git add -A');
|
|
69
|
+
exec('git commit -m "Initial commit from @temporalio/create"');
|
|
70
|
+
return true;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (didInit) {
|
|
73
|
+
try {
|
|
74
|
+
await rm(path.join(root, '.git'), { recursive: true, force: true });
|
|
75
|
+
} catch (_) {}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/install.ts
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
import { spawn } from './subprocess.js';
|
|
5
|
+
import { isUrlOk } from './samples.js';
|
|
6
|
+
|
|
7
|
+
interface InstallArgs {
|
|
8
|
+
root: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Indicate whether to install packages using Yarn.
|
|
12
|
+
*/
|
|
13
|
+
useYarn?: boolean;
|
|
14
|
+
temporalioVersion?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Spawn a package manager installation with either Yarn or NPM.
|
|
19
|
+
*
|
|
20
|
+
* @returns A Promise that resolves once the installation is finished.
|
|
21
|
+
*/
|
|
22
|
+
export function install({ root, useYarn }: InstallArgs): Promise<void> {
|
|
23
|
+
const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
|
|
24
|
+
const command: string = useYarn ? 'yarn' : npm;
|
|
25
|
+
|
|
26
|
+
return spawn(command, ['install'], {
|
|
27
|
+
cwd: root,
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
env: { ...process.env, ADBLOCK: '1', DISABLE_OPENCOLLECTIVE: '1' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function updateNodeVersion({ root }: InstallArgs): Promise<void> {
|
|
34
|
+
const currentNodeVersion = +process.versions.node.split('.')[0];
|
|
35
|
+
const versionAlreadyInPackageJson = 16;
|
|
36
|
+
const minimumValidVersion = 14;
|
|
37
|
+
|
|
38
|
+
if (currentNodeVersion >= minimumValidVersion && currentNodeVersion !== versionAlreadyInPackageJson) {
|
|
39
|
+
const packageName = `@tsconfig/node${currentNodeVersion}`;
|
|
40
|
+
const fileName = `${root}/package.json`;
|
|
41
|
+
|
|
42
|
+
const packageExists = await isUrlOk(`https://registry.npmjs.org/${packageName}`);
|
|
43
|
+
if (packageExists) {
|
|
44
|
+
let fileString = (await readFile(fileName)).toString();
|
|
45
|
+
await writeFile(fileName, fileString.replace(`@tsconfig/node${versionAlreadyInPackageJson}`, packageName));
|
|
46
|
+
|
|
47
|
+
const tsconfigJson = `${root}/tsconfig.json`;
|
|
48
|
+
fileString = (await readFile(tsconfigJson)).toString();
|
|
49
|
+
await writeFile(tsconfigJson, fileString.replace(`@tsconfig/node${versionAlreadyInPackageJson}`, packageName));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await writeFile(`${root}/.nvmrc`, currentNodeVersion.toString());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function replaceTemporalVersion({ root, temporalioVersion }: InstallArgs): Promise<void> {
|
|
57
|
+
const fileName = `${root}/package.json`;
|
|
58
|
+
|
|
59
|
+
const packageJson = JSON.parse(await readFile(fileName, 'utf8'));
|
|
60
|
+
packageJson.dependencies.temporalio = temporalioVersion;
|
|
61
|
+
await writeFile(fileName, JSON.stringify(packageJson));
|
|
62
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/is-online.ts
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import dns from 'dns';
|
|
4
|
+
import { URL } from 'url';
|
|
5
|
+
|
|
6
|
+
// Look for any proxy the user might have configured on their machine
|
|
7
|
+
function getProxy(): string | undefined {
|
|
8
|
+
if (process.env.https_proxy) {
|
|
9
|
+
return process.env.https_proxy;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const httpsProxy = execSync('npm config get https-proxy').toString().trim();
|
|
14
|
+
return httpsProxy !== 'null' ? httpsProxy : undefined;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function testIfThisComputerIsOnline(): Promise<boolean> {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
dns.lookup('github.com', (registryErr) => {
|
|
23
|
+
if (!registryErr) {
|
|
24
|
+
return resolve(true);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If we can't reach the registry directly, see if the user has a proxy
|
|
28
|
+
// configured. If they do, see if the proxy is reachable.
|
|
29
|
+
const proxy = getProxy();
|
|
30
|
+
if (!proxy) {
|
|
31
|
+
return resolve(false);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { hostname } = new URL(proxy);
|
|
35
|
+
if (!hostname) {
|
|
36
|
+
return resolve(false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dns.lookup(hostname, (proxyErr) => {
|
|
40
|
+
resolve(proxyErr == null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/is-writeable.ts
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
import { getErrorCode } from './get-error-code.js';
|
|
5
|
+
|
|
6
|
+
export async function isWriteable(directory: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await fs.promises.access(directory, (fs.constants || fs).W_OK);
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (getErrorCode(error) === 'EACCES') {
|
|
12
|
+
return false;
|
|
13
|
+
} else {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/make-dir.ts
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function makeDir(root: string, options = { recursive: true }): Promise<void> {
|
|
5
|
+
await fs.promises.mkdir(root, options);
|
|
6
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/examples.ts
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import got from 'got';
|
|
4
|
+
import tar from 'tar';
|
|
5
|
+
import { Stream } from 'stream';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import { rm, readdir } from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { getErrorCode } from './get-error-code.js';
|
|
10
|
+
|
|
11
|
+
const pipeline = promisify(Stream.pipeline);
|
|
12
|
+
|
|
13
|
+
export type RepoInfo = {
|
|
14
|
+
username: string;
|
|
15
|
+
name: string;
|
|
16
|
+
branch: string;
|
|
17
|
+
filePath: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function isUrlOk(url: string): Promise<boolean> {
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await got.head(url);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return res.statusCode === 200;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// https://stackoverflow.com/a/3561711/627729
|
|
31
|
+
function escapeRegex(s: string) {
|
|
32
|
+
// eslint-disable-next-line no-useless-escape
|
|
33
|
+
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getRepoInfo(url: URL, samplePath?: string): Promise<RepoInfo> {
|
|
37
|
+
const [, username, name, t, _branch, ...file] = url.pathname.split('/');
|
|
38
|
+
const filePath = samplePath ? samplePath.replace(/^\//, '') : file.join('/');
|
|
39
|
+
|
|
40
|
+
// Support repos whose entire purpose is to be a Temporal sample, e.g.
|
|
41
|
+
// https://github.com/:username/:my-cool-temporal-sample-repo
|
|
42
|
+
if (t === undefined) {
|
|
43
|
+
const repo = `https://api.github.com/repos/${username}/${name}`;
|
|
44
|
+
let infoResponse;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// https://github.com/sindresorhus/got/blob/main/documentation/3-streams.md#response-1
|
|
48
|
+
infoResponse = await got(repo);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(`Unable to fetch ${repo}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (infoResponse.statusCode !== 200) {
|
|
54
|
+
throw new Error(`Unable to fetch ${repo} — Code ${infoResponse.statusCode}: ${infoResponse.statusMessage}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const info = JSON.parse(infoResponse.body);
|
|
58
|
+
return { username, name, branch: info['default_branch'], filePath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If samplePath is available, the branch name takes the entire path
|
|
62
|
+
const branch = samplePath
|
|
63
|
+
? `${_branch}/${file.join('/')}`.replace(new RegExp(`/${escapeRegex(filePath)}|/$`), '')
|
|
64
|
+
: _branch;
|
|
65
|
+
|
|
66
|
+
if (username && name && branch && t === 'tree') {
|
|
67
|
+
return { username, name, branch, filePath };
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error(`Unable to parse URL: ${url} and sample path: ${samplePath}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function checkForPackageJson({ username, name, branch, filePath }: RepoInfo): Promise<void> {
|
|
74
|
+
const contentsUrl = `https://api.github.com/repos/${username}/${name}/contents`;
|
|
75
|
+
const packagePath = `${filePath ? `/${filePath}` : ''}/package.json`;
|
|
76
|
+
|
|
77
|
+
const fullUrl = contentsUrl + packagePath + `?ref=${branch}`;
|
|
78
|
+
|
|
79
|
+
if (!(await isUrlOk(fullUrl))) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Could not locate a package.json at ${chalk.red(
|
|
82
|
+
`"${fullUrl}"`
|
|
83
|
+
)}.\nPlease check that the repository is a Temporal TypeScript SDK template and try again.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function hasSample(name: string): Promise<boolean> {
|
|
89
|
+
return isUrlOk(
|
|
90
|
+
`https://api.github.com/repos/temporalio/samples-typescript/contents/${encodeURIComponent(name)}/package.json`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function downloadAndExtractRepo(
|
|
95
|
+
root: string,
|
|
96
|
+
{ username, name, branch, filePath }: RepoInfo
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const archiveUrl = `https://codeload.github.com/${username}/${name}/tar.gz/${branch}`;
|
|
99
|
+
const archivePath = `${name}-${branch}${filePath ? `/${filePath}` : ''}`;
|
|
100
|
+
|
|
101
|
+
await pipeline(
|
|
102
|
+
got.stream(archiveUrl),
|
|
103
|
+
tar.extract({ cwd: root, strip: filePath ? filePath.split('/').length + 1 : 1 }, [archivePath])
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const files = await readdir(root);
|
|
107
|
+
const pipelineFailed = files.length === 0;
|
|
108
|
+
if (pipelineFailed) {
|
|
109
|
+
console.error(
|
|
110
|
+
'We were unable to download and extract the provided project.\n',
|
|
111
|
+
`Archive URL: ${archiveUrl}\n`,
|
|
112
|
+
`Archive path: ${archivePath}\n`,
|
|
113
|
+
`Sometimes this is due to the repo name changing. If that's the case, try using the new repo URL.`
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function downloadAndExtractSample(root: string, name: string): Promise<void> {
|
|
120
|
+
if (name === '__internal-testing-retry') {
|
|
121
|
+
throw new Error('This is an internal sample for testing the CLI.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await pipeline(
|
|
125
|
+
got.stream('https://codeload.github.com/temporalio/samples-typescript/tar.gz/main'),
|
|
126
|
+
tar.extract({ cwd: root, strip: 2 }, [`samples-typescript-main/${name}`])
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await rm(path.join(root, `.npmrc`));
|
|
131
|
+
} catch (e) {
|
|
132
|
+
if (getErrorCode(e) !== 'ENOENT') {
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import glob from 'glob';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
4
|
+
|
|
5
|
+
export async function stripSnipComments(root: string): Promise<void> {
|
|
6
|
+
const files = glob.sync('**/*.ts', { cwd: root });
|
|
7
|
+
await Promise.all(
|
|
8
|
+
files.map(async (file) => {
|
|
9
|
+
const filePath = path.join(root, file);
|
|
10
|
+
const fileString = await readFile(filePath, 'utf8');
|
|
11
|
+
await writeFile(filePath, fileString.replace(/ *\/\/ @@@SNIP.+\n/g, ''));
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
|
|
2
|
+
import { ChildProcess, spawn as origSpawn, SpawnOptions } from 'child_process';
|
|
3
|
+
|
|
4
|
+
export class ChildProcessError extends Error {
|
|
5
|
+
public readonly name = 'ChildProcessError';
|
|
6
|
+
public command?: string;
|
|
7
|
+
public args?: ReadonlyArray<string>;
|
|
8
|
+
|
|
9
|
+
constructor(message: string, public readonly code: number | null, public readonly signal: string | null) {
|
|
10
|
+
super(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function spawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
// Workaround @types/node - avoid choosing overloads per options.stdio variants
|
|
17
|
+
await waitOnChild(options === undefined ? origSpawn(command, args) : origSpawn(command, args || [], options));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (err instanceof ChildProcessError) {
|
|
20
|
+
err.command = command;
|
|
21
|
+
err.args = args;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function waitOnChild(child: ChildProcess): Promise<void> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
child.on('exit', (code, signal) => {
|
|
30
|
+
if (code === 0) {
|
|
31
|
+
resolve();
|
|
32
|
+
} else {
|
|
33
|
+
reject(new ChildProcessError('Process failed', code, signal));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
child.on('error', reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/helpers/validate-pkg.ts
|
|
2
|
+
import validateProjectName from 'validate-npm-package-name';
|
|
3
|
+
|
|
4
|
+
export function validateNpmName(name: string): {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
problems?: string[];
|
|
7
|
+
} {
|
|
8
|
+
const nameValidation = validateProjectName(name);
|
|
9
|
+
if (nameValidation.validForNewPackages) {
|
|
10
|
+
return { valid: true };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
problems: [...(nameValidation.errors || []), ...(nameValidation.warnings || [])],
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Modified from: https://github.com/vercel/next.js/blob/2425f4703c4c6164cecfdb6aa8f80046213f0cc6/packages/create-next-app/index.ts
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import prompts from 'prompts';
|
|
7
|
+
import checkForUpdate from 'update-check';
|
|
8
|
+
|
|
9
|
+
import { createApp } from './create-project.js';
|
|
10
|
+
import { validateNpmName } from './helpers/validate-pkg.js';
|
|
11
|
+
import { fetchSamples } from './helpers/fetch-samples.js';
|
|
12
|
+
import packageJson from './pkg.js';
|
|
13
|
+
|
|
14
|
+
const program = new Command(packageJson.name)
|
|
15
|
+
.version(packageJson.version, '-v, --version', 'Print the version and exit')
|
|
16
|
+
.arguments('[project-directory]')
|
|
17
|
+
.usage(`${chalk.green('[project-directory]')} [options]`)
|
|
18
|
+
.option(
|
|
19
|
+
'-s, --sample <name|github-url>',
|
|
20
|
+
dedent`
|
|
21
|
+
Which sample to bootstrap the project with. You can use the name of a sample
|
|
22
|
+
from https://github.com/temporalio/samples-typescript or use a GitHub URL.
|
|
23
|
+
The URL can have a branch and/or subdirectory—for example:
|
|
24
|
+
https://github.com/temporalio/samples-typescript/tree/next/hello-world
|
|
25
|
+
`
|
|
26
|
+
)
|
|
27
|
+
.option(
|
|
28
|
+
'--sample-path <path-to-sample>',
|
|
29
|
+
dedent`
|
|
30
|
+
In a rare case, your GitHub URL might contain a branch name with
|
|
31
|
+
a slash (e.g. bug/fix-1) and the path to the sample (e.g. foo/bar).
|
|
32
|
+
In this case, you must specify the path to the sample separately:
|
|
33
|
+
--sample-path foo/bar
|
|
34
|
+
`
|
|
35
|
+
)
|
|
36
|
+
.option(
|
|
37
|
+
'-l, --list-samples',
|
|
38
|
+
dedent`
|
|
39
|
+
Print available sample projects and exit
|
|
40
|
+
`
|
|
41
|
+
)
|
|
42
|
+
.option(
|
|
43
|
+
'--use-yarn',
|
|
44
|
+
dedent`
|
|
45
|
+
Use yarn instead of npm
|
|
46
|
+
`
|
|
47
|
+
)
|
|
48
|
+
.option(
|
|
49
|
+
'--git-init',
|
|
50
|
+
dedent`
|
|
51
|
+
Initialize a git repository
|
|
52
|
+
`
|
|
53
|
+
)
|
|
54
|
+
.option(
|
|
55
|
+
'--no-git-init',
|
|
56
|
+
dedent`
|
|
57
|
+
Skip git repository initialization
|
|
58
|
+
`
|
|
59
|
+
)
|
|
60
|
+
.option(
|
|
61
|
+
'--temporalio-version <version>',
|
|
62
|
+
dedent`
|
|
63
|
+
Specify which version of the temporalio npm package to use
|
|
64
|
+
`
|
|
65
|
+
)
|
|
66
|
+
.allowUnknownOption()
|
|
67
|
+
.parse(process.argv);
|
|
68
|
+
|
|
69
|
+
interface Options {
|
|
70
|
+
useYarn?: boolean;
|
|
71
|
+
gitInit?: boolean;
|
|
72
|
+
listSamples?: boolean;
|
|
73
|
+
sample?: string;
|
|
74
|
+
samplePath?: string;
|
|
75
|
+
temporalioVersion?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let opts: Options;
|
|
79
|
+
|
|
80
|
+
async function start(): Promise<void> {
|
|
81
|
+
opts = program.opts();
|
|
82
|
+
if (opts.listSamples) {
|
|
83
|
+
const samples = await fetchSamples();
|
|
84
|
+
console.log(`Available samples:\n\n${samples.join('\n')}\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let projectPath = program.args[0];
|
|
89
|
+
|
|
90
|
+
if (typeof projectPath === 'string') {
|
|
91
|
+
projectPath = projectPath.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!projectPath) {
|
|
95
|
+
const res = await prompts({
|
|
96
|
+
type: 'text',
|
|
97
|
+
name: 'path',
|
|
98
|
+
message: 'What is your project named?',
|
|
99
|
+
initial: 'my-temporal',
|
|
100
|
+
validate: (name) => {
|
|
101
|
+
const validation = validateNpmName(path.basename(path.resolve(name)));
|
|
102
|
+
if (validation.valid) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return 'Invalid project name: ' + validation.problems?.[0];
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (typeof res.path === 'string') {
|
|
110
|
+
projectPath = res.path.trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!projectPath) {
|
|
115
|
+
console.error();
|
|
116
|
+
console.error('Please specify the project directory:');
|
|
117
|
+
console.error(` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`);
|
|
118
|
+
console.error();
|
|
119
|
+
console.error('For example:');
|
|
120
|
+
console.error(` ${chalk.cyan(program.name())} ${chalk.green('my-temporal-project')}`);
|
|
121
|
+
console.error();
|
|
122
|
+
console.error(`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
127
|
+
const projectName = path.basename(resolvedProjectPath);
|
|
128
|
+
|
|
129
|
+
const { valid, problems } = validateNpmName(projectName);
|
|
130
|
+
if (!valid) {
|
|
131
|
+
console.error(
|
|
132
|
+
`Could not create a project called ${chalk.red(`"${projectName}"`)} because of npm naming restrictions:`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
problems?.forEach((p) => console.error(` ${chalk.red.bold('*')} ${p}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let sample = opts.sample;
|
|
140
|
+
if (!sample) {
|
|
141
|
+
const samples = await fetchSamples();
|
|
142
|
+
const choices = samples.map((sample) => ({ title: sample, value: sample }));
|
|
143
|
+
|
|
144
|
+
const res = await prompts({
|
|
145
|
+
type: 'select',
|
|
146
|
+
name: 'sample',
|
|
147
|
+
message: `Which sample would you like to use?`,
|
|
148
|
+
choices,
|
|
149
|
+
initial: samples.indexOf('hello-world'),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (typeof res.sample === 'string') {
|
|
153
|
+
sample = res.sample;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!sample) {
|
|
158
|
+
console.error();
|
|
159
|
+
console.error('Please specify which sample:');
|
|
160
|
+
console.error(` ${chalk.cyan(program.name())} --sample ${chalk.green('<name|github-url>')}`);
|
|
161
|
+
console.error();
|
|
162
|
+
console.error('For example:');
|
|
163
|
+
console.error(` ${chalk.cyan(program.name())} --sample ${chalk.green('hello-world')}`);
|
|
164
|
+
console.error();
|
|
165
|
+
console.error(`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await createApp({
|
|
170
|
+
appPath: resolvedProjectPath,
|
|
171
|
+
useYarn: !!opts.useYarn,
|
|
172
|
+
gitInit: opts.gitInit,
|
|
173
|
+
temporalioVersion: opts.temporalioVersion,
|
|
174
|
+
sample: sample.trim(),
|
|
175
|
+
samplePath: typeof opts.samplePath === 'string' ? opts.samplePath.trim() : undefined,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const update = checkForUpdate(packageJson).catch(() => null);
|
|
180
|
+
|
|
181
|
+
async function notifyUpdate(): Promise<void> {
|
|
182
|
+
try {
|
|
183
|
+
const res = await update;
|
|
184
|
+
if (res?.latest) {
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk.yellow.bold('A new version of `@temporalio/create` is available!'));
|
|
187
|
+
console.log(
|
|
188
|
+
'You can update by running: ' +
|
|
189
|
+
chalk.cyan(opts.useYarn ? 'yarn global add @temporalio/create' : 'npm i -g @temporalio/create')
|
|
190
|
+
);
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
process.exit();
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function run(): void {
|
|
200
|
+
start()
|
|
201
|
+
.then(notifyUpdate)
|
|
202
|
+
.catch(async (reason) => {
|
|
203
|
+
console.log();
|
|
204
|
+
console.log('Aborting installation.');
|
|
205
|
+
if (reason.command) {
|
|
206
|
+
console.log(` ${chalk.cyan(reason.command)} has failed.`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.red('Unexpected error. Please report it as a bug:'));
|
|
209
|
+
console.log(reason);
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
|
|
213
|
+
await notifyUpdate();
|
|
214
|
+
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
217
|
+
}
|