create-svc 0.1.37 → 0.1.39
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/package.json +1 -1
- package/src/cli.ts +16 -1
- package/src/gcp.test.ts +75 -1
- package/src/gcp.ts +66 -0
- package/src/service-runtime/cloudrun/cleanup.ts +43 -4
- package/src/service-runtime/workers/cli.ts +44 -1
- package/src/service.test.ts +13 -3
- package/src/service.ts +41 -2
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -11,7 +11,13 @@ import {
|
|
|
11
11
|
markGitHubRepositoryDeleteOnDestroy,
|
|
12
12
|
type GitBootstrapResult,
|
|
13
13
|
} from "./git-bootstrap";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
assertExistingProjectReadyForAutoDeploy,
|
|
16
|
+
listOpenBillingAccounts,
|
|
17
|
+
listAccessibleProjects,
|
|
18
|
+
type BillingAccount,
|
|
19
|
+
type GcpProject,
|
|
20
|
+
} from "./gcp";
|
|
15
21
|
import {
|
|
16
22
|
BILLING_ACCOUNT_DEFAULT,
|
|
17
23
|
QUOTA_PROJECT_DEFAULT,
|
|
@@ -91,6 +97,7 @@ export async function run(argv: string[]) {
|
|
|
91
97
|
|
|
92
98
|
const config = await resolveConfig(args);
|
|
93
99
|
const targetDir = resolve(process.cwd(), config.directory);
|
|
100
|
+
await assertPreScaffoldReady(config);
|
|
94
101
|
|
|
95
102
|
note(
|
|
96
103
|
[
|
|
@@ -959,6 +966,14 @@ export function assertDiscoveryReady(discovery: DiscoveryState) {
|
|
|
959
966
|
return discovery;
|
|
960
967
|
}
|
|
961
968
|
|
|
969
|
+
async function assertPreScaffoldReady(config: ScaffoldConfig) {
|
|
970
|
+
if (config.target !== "cloudrun" || !config.autoDeploy || config.gcpProjectMode !== "use_existing") {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
await assertExistingProjectReadyForAutoDeploy(config.gcpProject);
|
|
975
|
+
}
|
|
976
|
+
|
|
962
977
|
function chooseBillingAccount(input: string | undefined, accounts: BillingAccount[]) {
|
|
963
978
|
if (input) {
|
|
964
979
|
return input;
|
package/src/gcp.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertExistingProjectReadyForAutoDeploy,
|
|
4
|
+
attachBillingAccount,
|
|
5
|
+
createProject,
|
|
6
|
+
listAccessibleProjects,
|
|
7
|
+
listOpenBillingAccounts,
|
|
8
|
+
type GcpApi,
|
|
9
|
+
} from "./gcp";
|
|
3
10
|
|
|
4
11
|
test("listAccessibleProjects filters deleted projects and sorts by name", async () => {
|
|
5
12
|
const api: GcpApi = {
|
|
@@ -105,3 +112,70 @@ test("createProject and attachBillingAccount call the expected endpoints", async
|
|
|
105
112
|
expect(calls[0]).toBe("create:anmho-test:test");
|
|
106
113
|
expect(calls[1]).toBe("billing:anmho-test:billingAccounts/123");
|
|
107
114
|
});
|
|
115
|
+
|
|
116
|
+
test("assertExistingProjectReadyForAutoDeploy passes when project exists with billing", async () => {
|
|
117
|
+
const api: GcpApi = {
|
|
118
|
+
async listProjects() {
|
|
119
|
+
return [];
|
|
120
|
+
},
|
|
121
|
+
async listBillingAccounts() {
|
|
122
|
+
return [];
|
|
123
|
+
},
|
|
124
|
+
async describeProject(projectId) {
|
|
125
|
+
return { projectId, name: "services" };
|
|
126
|
+
},
|
|
127
|
+
async describeBillingProject() {
|
|
128
|
+
return { billingEnabled: true, billingAccountName: "billingAccounts/123" };
|
|
129
|
+
},
|
|
130
|
+
async createProject() {},
|
|
131
|
+
async attachBillingAccount() {},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await expect(assertExistingProjectReadyForAutoDeploy("anmho-services", api)).resolves.toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("assertExistingProjectReadyForAutoDeploy rejects missing projects", async () => {
|
|
138
|
+
const api: GcpApi = {
|
|
139
|
+
async listProjects() {
|
|
140
|
+
return [];
|
|
141
|
+
},
|
|
142
|
+
async listBillingAccounts() {
|
|
143
|
+
return [];
|
|
144
|
+
},
|
|
145
|
+
async describeProject() {
|
|
146
|
+
throw new Error("not found");
|
|
147
|
+
},
|
|
148
|
+
async describeBillingProject() {
|
|
149
|
+
return { billingEnabled: true };
|
|
150
|
+
},
|
|
151
|
+
async createProject() {},
|
|
152
|
+
async attachBillingAccount() {},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await expect(assertExistingProjectReadyForAutoDeploy("anmho-services", api)).rejects.toThrow(
|
|
156
|
+
"GCP project anmho-services does not exist or is not accessible"
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("assertExistingProjectReadyForAutoDeploy rejects projects without billing", async () => {
|
|
161
|
+
const api: GcpApi = {
|
|
162
|
+
async listProjects() {
|
|
163
|
+
return [];
|
|
164
|
+
},
|
|
165
|
+
async listBillingAccounts() {
|
|
166
|
+
return [];
|
|
167
|
+
},
|
|
168
|
+
async describeProject(projectId) {
|
|
169
|
+
return { projectId, name: "services" };
|
|
170
|
+
},
|
|
171
|
+
async describeBillingProject() {
|
|
172
|
+
return { billingEnabled: false };
|
|
173
|
+
},
|
|
174
|
+
async createProject() {},
|
|
175
|
+
async attachBillingAccount() {},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
await expect(assertExistingProjectReadyForAutoDeploy("anmho-services", api)).rejects.toThrow(
|
|
179
|
+
"GCP project anmho-services exists but billing is not enabled"
|
|
180
|
+
);
|
|
181
|
+
});
|
package/src/gcp.ts
CHANGED
|
@@ -10,9 +10,16 @@ export type BillingAccount = {
|
|
|
10
10
|
open: boolean;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
export type BillingProject = {
|
|
14
|
+
billingEnabled?: boolean;
|
|
15
|
+
billingAccountName?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
13
18
|
export type GcpApi = {
|
|
14
19
|
listProjects(): Promise<GcpProject[]>;
|
|
15
20
|
listBillingAccounts(): Promise<BillingAccount[]>;
|
|
21
|
+
describeProject?(projectId: string): Promise<GcpProject>;
|
|
22
|
+
describeBillingProject?(projectId: string): Promise<BillingProject>;
|
|
16
23
|
createProject(projectId: string, name: string): Promise<void>;
|
|
17
24
|
attachBillingAccount(projectId: string, billingAccountName: string): Promise<void>;
|
|
18
25
|
};
|
|
@@ -33,6 +40,20 @@ export function createGcpApi(): GcpApi {
|
|
|
33
40
|
);
|
|
34
41
|
},
|
|
35
42
|
|
|
43
|
+
async describeProject(projectId: string) {
|
|
44
|
+
return parseJson<GcpProject>(
|
|
45
|
+
runGcloud(["projects", "describe", projectId, "--format=json(projectId,name,lifecycleState)"]).stdout,
|
|
46
|
+
"GCP project"
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async describeBillingProject(projectId: string) {
|
|
51
|
+
return parseJson<BillingProject>(
|
|
52
|
+
runGcloud(["beta", "billing", "projects", "describe", projectId, "--format=json(billingEnabled,billingAccountName)"]).stdout,
|
|
53
|
+
"GCP project billing"
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
|
|
36
57
|
async createProject(projectId: string, name: string) {
|
|
37
58
|
runGcloud(["projects", "create", projectId, "--name", name]);
|
|
38
59
|
},
|
|
@@ -64,6 +85,46 @@ export async function attachBillingAccount(projectId: string, billingAccountName
|
|
|
64
85
|
await api.attachBillingAccount(projectId, billingAccountName);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
export async function assertExistingProjectReadyForAutoDeploy(projectId: string, api = createGcpApi()) {
|
|
89
|
+
try {
|
|
90
|
+
await api.describeProject?.(projectId);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
[
|
|
94
|
+
`GCP project ${projectId} does not exist or is not accessible.`,
|
|
95
|
+
"Create and enable billing on that project before one-shot create, pass --project-id <billed-project>, or pass --no-auto-deploy.",
|
|
96
|
+
formatErrorDetail(error),
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join("\n")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let billing: BillingProject;
|
|
104
|
+
try {
|
|
105
|
+
billing = (await api.describeBillingProject?.(projectId)) ?? {};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
[
|
|
109
|
+
`Unable to verify billing for GCP project ${projectId}.`,
|
|
110
|
+
"Fix billing access before one-shot create, pass --project-id <billed-project>, or pass --no-auto-deploy.",
|
|
111
|
+
formatErrorDetail(error),
|
|
112
|
+
]
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.join("\n")
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!billing.billingEnabled) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
[
|
|
121
|
+
`GCP project ${projectId} exists but billing is not enabled.`,
|
|
122
|
+
"Link billing before one-shot create, pass --project-id <billed-project>, or pass --no-auto-deploy.",
|
|
123
|
+
].join("\n")
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
67
128
|
function runGcloud(args: string[]) {
|
|
68
129
|
const result = Bun.spawnSync(["gcloud", ...args], {
|
|
69
130
|
stdout: "pipe",
|
|
@@ -87,6 +148,11 @@ function accountSortName(account: BillingAccount) {
|
|
|
87
148
|
return account.displayName || account.name;
|
|
88
149
|
}
|
|
89
150
|
|
|
151
|
+
function formatErrorDetail(error: unknown) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
return message ? `Details: ${message}` : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
function parseJson<T>(value: string, label: string): T {
|
|
91
157
|
try {
|
|
92
158
|
return JSON.parse(value) as T;
|
|
@@ -75,8 +75,9 @@ export async function cleanup(args = Bun.argv.slice(2)) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
await requireDestroyConfirmation(options.force);
|
|
78
|
+
const deleteGitHubRepository = await confirmGitHubRepositoryDeletion(plan.githubRepository, options.force);
|
|
78
79
|
|
|
79
|
-
await deletePlannedResources(plan);
|
|
80
|
+
await deletePlannedResources(plan, { deleteGitHubRepository });
|
|
80
81
|
|
|
81
82
|
if (options.destroyProject) {
|
|
82
83
|
await runStep(`Deleting GCP project ${config.project.id}`, () => deleteProject());
|
|
@@ -87,7 +88,7 @@ export async function cleanup(args = Bun.argv.slice(2)) {
|
|
|
87
88
|
return `Destroy finished for ${config.serviceName}`;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async function deletePlannedResources(plan: DestroyPlan) {
|
|
91
|
+
async function deletePlannedResources(plan: DestroyPlan, options: { deleteGitHubRepository: boolean }) {
|
|
91
92
|
const tasks: ParallelTask[] = [
|
|
92
93
|
{
|
|
93
94
|
label: "Stopping local dev resources",
|
|
@@ -135,7 +136,7 @@ async function deletePlannedResources(plan: DestroyPlan) {
|
|
|
135
136
|
},
|
|
136
137
|
];
|
|
137
138
|
|
|
138
|
-
if (plan.githubRepository) {
|
|
139
|
+
if (plan.githubRepository && options.deleteGitHubRepository) {
|
|
139
140
|
tasks.push({
|
|
140
141
|
label: `Deleting GitHub repository ${plan.githubRepository}`,
|
|
141
142
|
task: () => deleteGitHubRepository(plan.githubRepository!),
|
|
@@ -246,7 +247,10 @@ function planGitHubRepository(plan: DestroyPlan) {
|
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
plan.githubRepository = repository;
|
|
249
|
-
plan.
|
|
250
|
+
plan.skipped.push({
|
|
251
|
+
label: `GitHub repository ${repository}`,
|
|
252
|
+
detail: "owned by this service CLI run; deletion is optional and defaults to no",
|
|
253
|
+
});
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
function deleteGitHubRepository(repository: string) {
|
|
@@ -400,6 +404,41 @@ async function requireDestroyConfirmation(force: boolean) {
|
|
|
400
404
|
}
|
|
401
405
|
}
|
|
402
406
|
|
|
407
|
+
async function confirmGitHubRepositoryDeletion(repository: string | undefined, force: boolean) {
|
|
408
|
+
if (!repository) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (force) {
|
|
413
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!process.stdin.isTTY) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const deleteAnswer = await confirm({
|
|
422
|
+
message: `Delete GitHub repository ${repository}?`,
|
|
423
|
+
initialValue: false,
|
|
424
|
+
});
|
|
425
|
+
if (isCancel(deleteAnswer) || !deleteAnswer) {
|
|
426
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const confirmAnswer = await confirm({
|
|
431
|
+
message: `Confirm deleting GitHub repository ${repository}? This cannot be undone.`,
|
|
432
|
+
initialValue: false,
|
|
433
|
+
});
|
|
434
|
+
if (isCancel(confirmAnswer) || !confirmAnswer) {
|
|
435
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
403
442
|
if (import.meta.main) {
|
|
404
443
|
await runMain("Destroy", () => cleanup(Bun.argv.slice(2)));
|
|
405
444
|
}
|
|
@@ -97,7 +97,9 @@ export async function main(argv = Bun.argv.slice(2)) {
|
|
|
97
97
|
await requireDestroyConfirmation(rest.includes("--force"));
|
|
98
98
|
const wranglerArgs = rest.filter((arg) => arg !== "--force");
|
|
99
99
|
await stopLocalDev({ dockerCompose: false });
|
|
100
|
-
|
|
100
|
+
if (await confirmGitHubRepositoryDeletion(rest.includes("--force"))) {
|
|
101
|
+
deleteGitHubRepositoryIfOwned();
|
|
102
|
+
}
|
|
101
103
|
await deleteHyperdrive();
|
|
102
104
|
run("wrangler", ["delete", "--name", config.serviceName, "--force", ...wranglerArgs]);
|
|
103
105
|
await deleteNeonDatabase();
|
|
@@ -152,6 +154,47 @@ function deleteGitHubRepositoryIfOwned() {
|
|
|
152
154
|
run("gh", ["repo", "delete", repository, "--yes"]);
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
async function confirmGitHubRepositoryDeletion(force: boolean) {
|
|
158
|
+
const repository = `${config.git.owner}/${config.git.repository}`;
|
|
159
|
+
if (!config.git.deleteOnDestroy) {
|
|
160
|
+
log.step(
|
|
161
|
+
`Skipping GitHub repository ${repository}: ${
|
|
162
|
+
config.git.enabled ? `not created by this service CLI run; manual cleanup: ${manualGitHubDeleteCommand(repository)}` : "git disabled"
|
|
163
|
+
}`
|
|
164
|
+
);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (force) {
|
|
169
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!process.stdin.isTTY) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const deleteAnswer = await confirm({
|
|
178
|
+
message: `Delete GitHub repository ${repository}?`,
|
|
179
|
+
initialValue: false,
|
|
180
|
+
});
|
|
181
|
+
if (isCancel(deleteAnswer) || !deleteAnswer) {
|
|
182
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const confirmAnswer = await confirm({
|
|
187
|
+
message: `Confirm deleting GitHub repository ${repository}? This cannot be undone.`,
|
|
188
|
+
initialValue: false,
|
|
189
|
+
});
|
|
190
|
+
if (isCancel(confirmAnswer) || !confirmAnswer) {
|
|
191
|
+
log.step(`Keeping GitHub repository ${repository}; delete manually with: ${manualGitHubDeleteCommand(repository)}`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
155
198
|
function run(command: string, args: string[], options: { allowFailure?: boolean; capture?: boolean } = {}) {
|
|
156
199
|
if (!Bun.which(command)) {
|
|
157
200
|
throw new Error(`missing required command: ${command}`);
|
package/src/service.test.ts
CHANGED
|
@@ -2,13 +2,12 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { findGeneratedServiceRoot, generatedDependenciesInstalled, normalizeScaffoldArgs } from "./service";
|
|
5
|
+
import { findGeneratedServiceRoot, formatOutsideServiceCommandError, generatedDependenciesInstalled, normalizeScaffoldArgs } from "./service";
|
|
6
6
|
|
|
7
|
-
test("normalizeScaffoldArgs treats
|
|
7
|
+
test("normalizeScaffoldArgs treats explicit scaffold commands as generator commands", () => {
|
|
8
8
|
expect(normalizeScaffoldArgs(["create", "launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
9
9
|
expect(normalizeScaffoldArgs(["new", "launch-api"])).toEqual(["launch-api"]);
|
|
10
10
|
expect(normalizeScaffoldArgs(["init", "launch-api"])).toEqual(["launch-api"]);
|
|
11
|
-
expect(normalizeScaffoldArgs(["launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
12
11
|
});
|
|
13
12
|
|
|
14
13
|
test("normalizeScaffoldArgs maps service help to generator help outside a service repo", () => {
|
|
@@ -16,6 +15,17 @@ test("normalizeScaffoldArgs maps service help to generator help outside a servic
|
|
|
16
15
|
expect(normalizeScaffoldArgs(["help", "--verbose"])).toEqual(["--help", "--verbose"]);
|
|
17
16
|
});
|
|
18
17
|
|
|
18
|
+
test("formatOutsideServiceCommandError rejects repo-local commands outside generated services", () => {
|
|
19
|
+
expect(formatOutsideServiceCommandError("destroy")).toContain("service destroy must be run inside a generated service repo");
|
|
20
|
+
expect(formatOutsideServiceCommandError("deploy")).toContain("No service.jsonc was found");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("formatOutsideServiceCommandError does not treat positional names as scaffold commands", () => {
|
|
24
|
+
const message = formatOutsideServiceCommandError("launch-api");
|
|
25
|
+
expect(message).toContain("Unknown command: launch-api");
|
|
26
|
+
expect(message).toContain("service create <service_id>");
|
|
27
|
+
});
|
|
28
|
+
|
|
19
29
|
test("findGeneratedServiceRoot detects generated service context from nested directories", async () => {
|
|
20
30
|
const root = await mkdtemp(join(tmpdir(), "create-svc-service-root-"));
|
|
21
31
|
const serviceRoot = join(root, "generated-api");
|
package/src/service.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { run as runScaffoldCli } from "./cli";
|
|
3
|
+
import { formatScaffoldHelp, run as runScaffoldCli } from "./cli";
|
|
4
4
|
import { parseJsonc } from "./jsonc";
|
|
5
5
|
|
|
6
6
|
const SCAFFOLD_COMMANDS = new Set(["create", "new", "init"]);
|
|
7
|
+
const GENERATED_SERVICE_COMMANDS = new Set([
|
|
8
|
+
"auth",
|
|
9
|
+
"create",
|
|
10
|
+
"dashboards",
|
|
11
|
+
"deploy",
|
|
12
|
+
"destroy",
|
|
13
|
+
"dev",
|
|
14
|
+
"dns",
|
|
15
|
+
"doctor",
|
|
16
|
+
"migrate",
|
|
17
|
+
"sdk",
|
|
18
|
+
"seed",
|
|
19
|
+
]);
|
|
7
20
|
|
|
8
21
|
export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
|
|
9
22
|
const serviceRoot = findGeneratedServiceRoot(cwd);
|
|
@@ -12,7 +25,19 @@ export async function runServiceCommand(argv: string[], cwd = process.cwd()) {
|
|
|
12
25
|
return;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
const [command] = argv;
|
|
29
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
30
|
+
console.log(formatScaffoldHelp());
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (SCAFFOLD_COMMANDS.has(command)) {
|
|
35
|
+
await runScaffoldCli(normalizeScaffoldArgs(argv));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.error(formatOutsideServiceCommandError(command));
|
|
40
|
+
process.exit(1);
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
export function normalizeScaffoldArgs(argv: string[]) {
|
|
@@ -26,6 +51,20 @@ export function normalizeScaffoldArgs(argv: string[]) {
|
|
|
26
51
|
return argv;
|
|
27
52
|
}
|
|
28
53
|
|
|
54
|
+
export function formatOutsideServiceCommandError(command: string) {
|
|
55
|
+
if (GENERATED_SERVICE_COMMANDS.has(command)) {
|
|
56
|
+
return [
|
|
57
|
+
`service ${command} must be run inside a generated service repo.`,
|
|
58
|
+
"",
|
|
59
|
+
"No service.jsonc was found in this directory or its parents.",
|
|
60
|
+
"To create a new service, run:",
|
|
61
|
+
" service create <service_id>",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [`Unknown command: ${command}`, "", formatScaffoldHelp()].join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
export function findGeneratedServiceRoot(start: string): string | undefined {
|
|
30
69
|
let current = start;
|
|
31
70
|
while (true) {
|