create-svc 0.1.13 → 0.1.14
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 +19 -6
- package/package.json +1 -1
- package/src/cli.test.ts +31 -2
- package/src/cli.ts +116 -121
- package/src/git-bootstrap.ts +14 -11
- package/src/naming.test.ts +1 -1
- package/src/naming.ts +1 -1
- package/src/post-scaffold.test.ts +17 -1
- package/src/post-scaffold.ts +24 -3
- package/src/scaffold.test.ts +12 -5
- package/src/service.test.ts +12 -1
- package/src/service.ts +26 -0
- package/templates/shared/scripts/cloudrun/cli.ts +21 -2
- package/templates/targets/workers/scripts/workers/cli.ts +20 -2
- package/templates/variants/go-chi/package.json +9 -1
- package/templates/variants/go-connectrpc/package.json +9 -1
package/README.md
CHANGED
|
@@ -23,6 +23,13 @@ npm: <https://www.npmjs.com/package/create-svc>
|
|
|
23
23
|
service create my-service
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
That creates `./my-service` by default. To write somewhere else while keeping
|
|
27
|
+
the service id as `my-service`, pass `--dir`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
service create my-service --dir /Users/andrewho/repos/projects/my-service
|
|
31
|
+
```
|
|
32
|
+
|
|
26
33
|
Inside a generated service repo, the same command operates that repo:
|
|
27
34
|
|
|
28
35
|
```bash
|
|
@@ -34,7 +41,7 @@ service deploy
|
|
|
34
41
|
To install from npm:
|
|
35
42
|
|
|
36
43
|
```bash
|
|
37
|
-
|
|
44
|
+
npm install -g create-svc
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
For the strict one-command production path:
|
|
@@ -43,14 +50,19 @@ For the strict one-command production path:
|
|
|
43
50
|
service create my-service --yes
|
|
44
51
|
```
|
|
45
52
|
|
|
53
|
+
By default, that scaffolds the repo, installs dependencies, runs the generated
|
|
54
|
+
repo's `service create`, and then runs `service deploy`. Pass
|
|
55
|
+
`--no-auto-deploy` for scaffold-only generation.
|
|
56
|
+
|
|
46
57
|
`--profile microservice` is accepted as a compatibility no-op. App workspaces live outside this package in private app template repositories.
|
|
47
58
|
|
|
48
59
|
By default, a standalone generated service is initialized as a git repository,
|
|
49
60
|
committed with `Initial commit`, created as a private GitHub repository at
|
|
50
|
-
`anmho/<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
`https://github.com/anmho/<service_id>`, and pushed to `origin/main`. Go
|
|
62
|
+
services also default their module path to `github.com/anmho/<service_id>`.
|
|
63
|
+
If the target directory is inside an existing git worktree, `service` skips git
|
|
64
|
+
and GitHub setup so the parent repository remains in control. Pass `--no-git`
|
|
65
|
+
to skip all git and GitHub side effects.
|
|
54
66
|
|
|
55
67
|
## Local Testing
|
|
56
68
|
|
|
@@ -59,7 +71,8 @@ Without publishing to npm:
|
|
|
59
71
|
```bash
|
|
60
72
|
bun install
|
|
61
73
|
npm pack
|
|
62
|
-
|
|
74
|
+
npm install -g ./create-svc-*.tgz
|
|
75
|
+
service create my-service
|
|
63
76
|
```
|
|
64
77
|
|
|
65
78
|
For faster iteration against your working tree:
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import {
|
|
4
4
|
assertDiscoveryReady,
|
|
5
|
+
formatScaffoldHelp,
|
|
5
6
|
normalizeValidationResult,
|
|
6
7
|
parseArgs,
|
|
8
|
+
resolveAutoDeploy,
|
|
7
9
|
validateTargetRuntimeFramework,
|
|
8
10
|
validateServiceNameInput,
|
|
9
11
|
} from "./cli";
|
|
@@ -32,12 +34,12 @@ test("assertDiscoveryReady no longer blocks scaffold when remote discovery is un
|
|
|
32
34
|
|
|
33
35
|
test("parseArgs defaults to microservice and cloudrun target", () => {
|
|
34
36
|
expect(parseArgs(["launch-api", "--yes"])).toMatchObject({
|
|
35
|
-
|
|
37
|
+
serviceName: "launch-api",
|
|
36
38
|
profile: "microservice",
|
|
37
39
|
yes: true,
|
|
38
40
|
});
|
|
39
41
|
expect(parseArgs(["launch-api", "--target", "workers", "--yes"])).toMatchObject({
|
|
40
|
-
|
|
42
|
+
serviceName: "launch-api",
|
|
41
43
|
target: "workers",
|
|
42
44
|
yes: true,
|
|
43
45
|
});
|
|
@@ -47,6 +49,33 @@ test("parseArgs defaults to microservice and cloudrun target", () => {
|
|
|
47
49
|
expect(() => parseArgs(["launch-api", "--profile", "microservice", "--bootstrap"])).toThrow("Unknown argument");
|
|
48
50
|
});
|
|
49
51
|
|
|
52
|
+
test("resolveAutoDeploy defaults to one-shot create and deploy", () => {
|
|
53
|
+
expect(resolveAutoDeploy(undefined)).toBeTrue();
|
|
54
|
+
expect(resolveAutoDeploy(true)).toBeTrue();
|
|
55
|
+
expect(resolveAutoDeploy(false)).toBeFalse();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("parseArgs supports an explicit output directory", () => {
|
|
59
|
+
expect(parseArgs(["launch-api", "--dir", "/tmp/generated-launch-api", "--yes"])).toMatchObject({
|
|
60
|
+
serviceName: "launch-api",
|
|
61
|
+
directory: "/tmp/generated-launch-api",
|
|
62
|
+
yes: true,
|
|
63
|
+
});
|
|
64
|
+
expect(parseArgs(["--dir=/tmp/generated-launch-api", "--yes"])).toMatchObject({
|
|
65
|
+
directory: "/tmp/generated-launch-api",
|
|
66
|
+
yes: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("formatScaffoldHelp is compact and starts at usage", () => {
|
|
71
|
+
const help = formatScaffoldHelp();
|
|
72
|
+
expect(help.startsWith("Usage:\n")).toBeTrue();
|
|
73
|
+
expect(help).not.toContain("\n\n\n");
|
|
74
|
+
expect(help).not.toContain("│");
|
|
75
|
+
expect(help).toContain("service create <service_id> [options]");
|
|
76
|
+
expect(help).toContain("--dir <path>");
|
|
77
|
+
});
|
|
78
|
+
|
|
50
79
|
test("parseArgs rejects the removed app profile", () => {
|
|
51
80
|
expect(() => parseArgs(["tracker", "--profile=app", "--yes"])).toThrow("app profile has moved");
|
|
52
81
|
});
|
package/src/cli.ts
CHANGED
|
@@ -15,8 +15,13 @@ import pc from "picocolors";
|
|
|
15
15
|
import { readdirSync } from "node:fs";
|
|
16
16
|
import { basename, dirname, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { runPostScaffoldFlow } from "./post-scaffold";
|
|
19
|
-
import {
|
|
18
|
+
import { buildDeploymentVerificationCommands, runPostScaffoldFlow } from "./post-scaffold";
|
|
19
|
+
import {
|
|
20
|
+
bootstrapGitHubRepository,
|
|
21
|
+
buildGitBootstrapConfig,
|
|
22
|
+
commitAndPushGeneratedArtifacts,
|
|
23
|
+
type GitBootstrapResult,
|
|
24
|
+
} from "./git-bootstrap";
|
|
20
25
|
import { listOpenBillingAccounts, listAccessibleProjects, type BillingAccount, type GcpProject } from "./gcp";
|
|
21
26
|
import {
|
|
22
27
|
BILLING_ACCOUNT_DEFAULT,
|
|
@@ -39,6 +44,7 @@ import {
|
|
|
39
44
|
} from "./scaffold";
|
|
40
45
|
|
|
41
46
|
type ParsedArgs = {
|
|
47
|
+
serviceName?: string;
|
|
42
48
|
directory?: string;
|
|
43
49
|
target?: DeployTarget;
|
|
44
50
|
runtime?: Runtime;
|
|
@@ -50,8 +56,6 @@ type ParsedArgs = {
|
|
|
50
56
|
billingAccount?: string;
|
|
51
57
|
quotaProjectId?: string;
|
|
52
58
|
autoDeploy?: boolean;
|
|
53
|
-
autoUpdate?: boolean;
|
|
54
|
-
noUpdateCheck?: boolean;
|
|
55
59
|
noGit?: boolean;
|
|
56
60
|
profile: Profile;
|
|
57
61
|
yes: boolean;
|
|
@@ -74,8 +78,6 @@ export async function run(argv: string[]) {
|
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
await maybeCheckForUpdate(args);
|
|
78
|
-
|
|
79
81
|
intro(`${pc.bold("service")} ${pc.dim("microservice bootstrap")}`);
|
|
80
82
|
|
|
81
83
|
const config = await resolveConfig(args);
|
|
@@ -130,27 +132,75 @@ export async function run(argv: string[]) {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
[
|
|
136
|
-
`Next: ${pc.cyan(`cd ${config.directory}`)}`,
|
|
137
|
-
`Local DB: ${pc.cyan("started by local dev command")}`,
|
|
138
|
-
`Migrate: ${pc.cyan(isBun ? "bun run migrate" : "make migrate")}`,
|
|
139
|
-
`Local dev: ${pc.cyan(isBun ? "bun run dev" : "make dev")}`,
|
|
140
|
-
`Create: ${pc.cyan("service create")}`,
|
|
141
|
-
`Deploy: ${pc.cyan("service deploy")}`,
|
|
142
|
-
config.git.enabled ? `Repository: ${pc.cyan(`https://github.com/anmho/${config.git.repository}`)}` : undefined,
|
|
143
|
-
`Personal env: ${pc.cyan(
|
|
144
|
-
`service deploy --environment personal --name ${config.serviceName}`
|
|
145
|
-
)}`,
|
|
146
|
-
`Production API: ${pc.cyan(`https://${config.apiHostname}`)}`,
|
|
147
|
-
].filter(Boolean).join("\n")
|
|
148
|
-
);
|
|
135
|
+
outro(config.autoDeploy ? "Created and deployed" : "Created");
|
|
136
|
+
console.log(formatCompletionSummary(config, targetDir, gitResult));
|
|
149
137
|
} catch (error) {
|
|
150
138
|
handleCliError(error);
|
|
151
139
|
}
|
|
152
140
|
}
|
|
153
141
|
|
|
142
|
+
function formatCompletionSummary(config: ScaffoldConfig, targetDir: string, gitResult: GitBootstrapResult) {
|
|
143
|
+
const isBun = config.runtime === "bun";
|
|
144
|
+
const devCommand = isBun ? "bun run dev" : "make dev";
|
|
145
|
+
const migrateCommand = isBun ? "bun run migrate" : "make migrate";
|
|
146
|
+
const lifecycleCommands: Array<[string, string]> = config.autoDeploy
|
|
147
|
+
? [
|
|
148
|
+
["service deploy", "Deploys later changes."],
|
|
149
|
+
[`service deploy --environment personal --name ${config.serviceName}`, "Deploys your personal environment."],
|
|
150
|
+
]
|
|
151
|
+
: [
|
|
152
|
+
["service create", "Provisions auth, database, migrations, and the first deploy."],
|
|
153
|
+
["service deploy", "Deploys later changes."],
|
|
154
|
+
];
|
|
155
|
+
const repository =
|
|
156
|
+
gitResult.status === "created"
|
|
157
|
+
? gitResult.url
|
|
158
|
+
: config.git.enabled
|
|
159
|
+
? `https://github.com/${config.git.owner}/${config.git.repository}`
|
|
160
|
+
: undefined;
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
"",
|
|
164
|
+
`Success! Created ${config.serviceName} at ${targetDir}`,
|
|
165
|
+
"",
|
|
166
|
+
"Inside that directory, you can run:",
|
|
167
|
+
formatCommand(devCommand, "Starts local development."),
|
|
168
|
+
formatCommand(migrateCommand, "Applies local database migrations."),
|
|
169
|
+
...lifecycleCommands.map(([command, description]) => formatCommand(command, description)),
|
|
170
|
+
"",
|
|
171
|
+
"Control-plane defaults:",
|
|
172
|
+
` Auth issuer: https://auth.anmho.com/api/auth`,
|
|
173
|
+
` Auth resource: api://${config.serviceName}`,
|
|
174
|
+
` Auth token URL: https://auth.anmho.com/api/auth/oauth2/token`,
|
|
175
|
+
` Temporal: disabled by default`,
|
|
176
|
+
` Temporal address: localhost:7233`,
|
|
177
|
+
` Temporal task queue: ${config.serviceName}`,
|
|
178
|
+
` Temporal API key secret: ${config.serviceName}-temporal-api-key`,
|
|
179
|
+
config.runtime === "go" ? ` Go module: ${config.modulePath}` : undefined,
|
|
180
|
+
"",
|
|
181
|
+
config.autoDeploy ? "Verified after deploy:" : "After deploy, verify with:",
|
|
182
|
+
...buildDeploymentVerificationCommands(config).map(formatShellCommand),
|
|
183
|
+
"",
|
|
184
|
+
"We suggest that you begin by typing:",
|
|
185
|
+
"",
|
|
186
|
+
` cd ${config.directory}`,
|
|
187
|
+
` ${devCommand}`,
|
|
188
|
+
"",
|
|
189
|
+
repository ? `Repository: ${repository}` : undefined,
|
|
190
|
+
`Production API: https://${config.apiHostname}`,
|
|
191
|
+
]
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatCommand(command: string, description: string) {
|
|
197
|
+
return [` ${command}`, ` ${description}`].join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatShellCommand(command: { command: string; args: string[] }) {
|
|
201
|
+
return ` ${[command.command, ...command.args].join(" ")}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
155
205
|
const parsed: ParsedArgs = {
|
|
156
206
|
profile: "microservice",
|
|
@@ -164,8 +214,8 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
164
214
|
continue;
|
|
165
215
|
}
|
|
166
216
|
|
|
167
|
-
if (!token.startsWith("-") && !parsed.
|
|
168
|
-
parsed.
|
|
217
|
+
if (!token.startsWith("-") && !parsed.serviceName) {
|
|
218
|
+
parsed.serviceName = token;
|
|
169
219
|
continue;
|
|
170
220
|
}
|
|
171
221
|
|
|
@@ -188,23 +238,23 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
188
238
|
continue;
|
|
189
239
|
}
|
|
190
240
|
|
|
191
|
-
if (token === "--
|
|
192
|
-
parsed.
|
|
241
|
+
if (token === "--no-git") {
|
|
242
|
+
parsed.noGit = true;
|
|
193
243
|
continue;
|
|
194
244
|
}
|
|
195
245
|
|
|
196
|
-
if (token === "--
|
|
197
|
-
parsed.
|
|
246
|
+
if (token === "--runtime") {
|
|
247
|
+
parsed.runtime = readValue() as Runtime;
|
|
198
248
|
continue;
|
|
199
249
|
}
|
|
200
250
|
|
|
201
|
-
if (token === "--
|
|
202
|
-
parsed.
|
|
251
|
+
if (token === "--dir") {
|
|
252
|
+
parsed.directory = readValue();
|
|
203
253
|
continue;
|
|
204
254
|
}
|
|
205
255
|
|
|
206
|
-
if (token
|
|
207
|
-
parsed.
|
|
256
|
+
if (token.startsWith("--dir=")) {
|
|
257
|
+
parsed.directory = token.slice("--dir=".length);
|
|
208
258
|
continue;
|
|
209
259
|
}
|
|
210
260
|
|
|
@@ -324,71 +374,8 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
324
374
|
return parsed;
|
|
325
375
|
}
|
|
326
376
|
|
|
327
|
-
const CURRENT_VERSION = "0.1.9";
|
|
328
|
-
const PACKAGE_NAME = "create-svc";
|
|
329
|
-
|
|
330
|
-
async function maybeCheckForUpdate(args: ParsedArgs) {
|
|
331
|
-
if (args.noUpdateCheck || shouldSkipUpdateCheck()) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const latest = await resolveLatestVersion().catch(() => "");
|
|
336
|
-
if (!latest || !isVersionGreater(latest, CURRENT_VERSION)) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const command = `bunx ${PACKAGE_NAME}@latest ${Bun.argv.slice(2).filter((arg) => arg !== "--auto-update").join(" ")}`.trim();
|
|
341
|
-
if (!args.autoUpdate) {
|
|
342
|
-
log.info(`A newer ${PACKAGE_NAME} is available: ${CURRENT_VERSION} -> ${latest}. Run ${command}`);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const result = Bun.spawnSync(["bunx", `${PACKAGE_NAME}@latest`, ...Bun.argv.slice(2).filter((arg) => arg !== "--auto-update")], {
|
|
347
|
-
stdin: "inherit",
|
|
348
|
-
stdout: "inherit",
|
|
349
|
-
stderr: "inherit",
|
|
350
|
-
env: {
|
|
351
|
-
...process.env,
|
|
352
|
-
CREATE_SERVICE_NO_UPDATE_CHECK: "1",
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
process.exit(result.exitCode);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function shouldSkipUpdateCheck() {
|
|
359
|
-
return Boolean(
|
|
360
|
-
process.env.CI ||
|
|
361
|
-
process.env.CODEX_CI ||
|
|
362
|
-
process.env.CREATE_SERVICE_NO_UPDATE_CHECK ||
|
|
363
|
-
process.env.BUN_TEST ||
|
|
364
|
-
process.env.npm_lifecycle_event
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async function resolveLatestVersion() {
|
|
369
|
-
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
370
|
-
signal: AbortSignal.timeout(1_500),
|
|
371
|
-
});
|
|
372
|
-
if (!response.ok) {
|
|
373
|
-
return "";
|
|
374
|
-
}
|
|
375
|
-
const payload = (await response.json()) as { version?: string };
|
|
376
|
-
return payload.version?.trim() ?? "";
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function isVersionGreater(left: string, right: string) {
|
|
380
|
-
const parse = (value: string) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
381
|
-
const [leftMajor = 0, leftMinor = 0, leftPatch = 0] = parse(left);
|
|
382
|
-
const [rightMajor = 0, rightMinor = 0, rightPatch = 0] = parse(right);
|
|
383
|
-
return (
|
|
384
|
-
leftMajor > rightMajor ||
|
|
385
|
-
(leftMajor === rightMajor && leftMinor > rightMinor) ||
|
|
386
|
-
(leftMajor === rightMajor && leftMinor === rightMinor && leftPatch > rightPatch)
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
377
|
export async function resolveConfig(args: ParsedArgs): Promise<ScaffoldConfig> {
|
|
391
|
-
const inferredName = slugify(basename(args.directory ?? "my-service"));
|
|
378
|
+
const inferredName = slugify(args.serviceName ?? basename(args.directory ?? "my-service"));
|
|
392
379
|
const serviceName = args.yes
|
|
393
380
|
? inferredName
|
|
394
381
|
: await promptText("Service name", inferredName, (value) => validateServiceNameInput(value, args.directory));
|
|
@@ -704,11 +691,11 @@ function chooseBillingAccount(input: string | undefined, accounts: BillingAccoun
|
|
|
704
691
|
return accounts[0]?.name ?? BILLING_ACCOUNT_DEFAULT;
|
|
705
692
|
}
|
|
706
693
|
|
|
707
|
-
function resolveAutoDeploy(value: boolean | undefined) {
|
|
694
|
+
export function resolveAutoDeploy(value: boolean | undefined) {
|
|
708
695
|
if (value !== undefined) {
|
|
709
696
|
return value;
|
|
710
697
|
}
|
|
711
|
-
return
|
|
698
|
+
return true;
|
|
712
699
|
}
|
|
713
700
|
|
|
714
701
|
async function promptText(
|
|
@@ -823,29 +810,37 @@ export function validateServiceNameInput(rawValue: string, directoryOverride?: s
|
|
|
823
810
|
}
|
|
824
811
|
|
|
825
812
|
function printHelp() {
|
|
826
|
-
log
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
--
|
|
840
|
-
--
|
|
841
|
-
--
|
|
842
|
-
--
|
|
843
|
-
--
|
|
844
|
-
--
|
|
845
|
-
--
|
|
846
|
-
--
|
|
847
|
-
--
|
|
848
|
-
|
|
813
|
+
console.log(formatScaffoldHelp());
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function formatScaffoldHelp() {
|
|
817
|
+
return [
|
|
818
|
+
"Usage:",
|
|
819
|
+
" service create <service_id> [options]",
|
|
820
|
+
"",
|
|
821
|
+
"Examples:",
|
|
822
|
+
" service create waitlist-api --target cloudrun --runtime bun --framework hono",
|
|
823
|
+
" service create waitlist-api --auto-deploy",
|
|
824
|
+
"",
|
|
825
|
+
"Options:",
|
|
826
|
+
" --dir <path> Output directory; defaults to ./<service_id>",
|
|
827
|
+
" --target <cloudrun|workers> Deploy target for the generated service",
|
|
828
|
+
" --runtime <go|bun> Runtime scaffold to generate",
|
|
829
|
+
" --framework <name> Framework for the selected runtime",
|
|
830
|
+
" --module-path <path> Go module path for generated Go scaffolds",
|
|
831
|
+
" --project-mode <mode> create_new or use_existing",
|
|
832
|
+
" --project-id <id> GCP project id",
|
|
833
|
+
" --billing-account <name> Billing account resource name",
|
|
834
|
+
" --quota-project <id> Billing quota project for gcloud calls",
|
|
835
|
+
" --region <region> Cloud Run region",
|
|
836
|
+
" --auto-deploy Scaffold, run service create, then service deploy (default)",
|
|
837
|
+
" --no-auto-deploy Scaffold only",
|
|
838
|
+
" --no-git Skip default private GitHub repo: anmho/<service_id>",
|
|
839
|
+
" --yes, -y Accept defaults without prompts",
|
|
840
|
+
" --help, -h Show this message",
|
|
841
|
+
"",
|
|
842
|
+
"Inside a generated service repo, run service --help for create, deploy, doctor, auth, and sdk commands.",
|
|
843
|
+
].join("\n");
|
|
849
844
|
}
|
|
850
845
|
|
|
851
846
|
function matchesProject(project: GcpProject, query: string) {
|
package/src/git-bootstrap.ts
CHANGED
|
@@ -30,19 +30,21 @@ export async function bootstrapGitHubRepository(targetDir: string, config: GitBo
|
|
|
30
30
|
return { status: "skipped-existing-worktree", root: existingRoot };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
run(["git", "--version"], targetDir, "git is required to initialize the generated repository");
|
|
34
|
-
run(["gh", "--version"], targetDir, "GitHub CLI `gh` is required to create the generated repository");
|
|
35
|
-
run(["gh", "auth", "status"], targetDir, "Authenticate GitHub CLI with `gh auth login` before creating the repository");
|
|
33
|
+
run(["git", "--version"], targetDir, "git is required to initialize the generated repository", { quiet: true });
|
|
34
|
+
run(["gh", "--version"], targetDir, "GitHub CLI `gh` is required to create the generated repository", { quiet: true });
|
|
35
|
+
run(["gh", "auth", "status"], targetDir, "Authenticate GitHub CLI with `gh auth login` before creating the repository", { quiet: true });
|
|
36
36
|
|
|
37
|
-
run(["git", "init", "-b", "main"], targetDir);
|
|
37
|
+
run(["git", "init", "-b", "main", "--quiet"], targetDir);
|
|
38
38
|
run(["git", "add", "."], targetDir);
|
|
39
39
|
|
|
40
40
|
if (hasStagedChanges(targetDir)) {
|
|
41
|
-
run(["git", "commit", "-m", "Initial commit"], targetDir);
|
|
41
|
+
run(["git", "commit", "--quiet", "-m", "Initial commit"], targetDir);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const repository = `${config.owner}/${config.repository}`;
|
|
45
|
-
run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir
|
|
45
|
+
run(["gh", "repo", "create", repository, "--private", "--source", ".", "--remote", "origin", "--push"], targetDir, undefined, {
|
|
46
|
+
quiet: true,
|
|
47
|
+
});
|
|
46
48
|
|
|
47
49
|
return {
|
|
48
50
|
status: "created",
|
|
@@ -55,8 +57,8 @@ export function commitAndPushGeneratedArtifacts(targetDir: string, message: stri
|
|
|
55
57
|
if (!hasStagedChanges(targetDir)) {
|
|
56
58
|
return { committed: false };
|
|
57
59
|
}
|
|
58
|
-
run(["git", "commit", "-m", message], targetDir);
|
|
59
|
-
run(["git", "push"], targetDir);
|
|
60
|
+
run(["git", "commit", "--quiet", "-m", message], targetDir);
|
|
61
|
+
run(["git", "push", "--quiet"], targetDir, undefined, { quiet: true });
|
|
60
62
|
return { committed: true };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -94,17 +96,18 @@ function hasStagedChanges(cwd: string) {
|
|
|
94
96
|
return result.exitCode === 1;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
function run(command: string[], cwd: string, message?: string) {
|
|
99
|
+
function run(command: string[], cwd: string, message?: string, options: { quiet?: boolean } = {}) {
|
|
98
100
|
const result = Bun.spawnSync(command, {
|
|
99
101
|
cwd,
|
|
100
102
|
stdin: "inherit",
|
|
101
|
-
stdout: "inherit",
|
|
103
|
+
stdout: options.quiet ? "pipe" : "inherit",
|
|
102
104
|
stderr: "pipe",
|
|
103
105
|
});
|
|
104
106
|
if (result.exitCode === 0) {
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
const output = result.stdout?.toString().trim() ?? "";
|
|
108
111
|
const detail = result.stderr.toString().trim();
|
|
109
|
-
throw new Error([message, `Command failed: ${command.join(" ")}`, detail].filter(Boolean).join("\n"));
|
|
112
|
+
throw new Error([message, `Command failed: ${command.join(" ")}`, output, detail].filter(Boolean).join("\n"));
|
|
110
113
|
}
|
package/src/naming.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ test("deriveDefaults uses the service name for project, repo, and database namin
|
|
|
11
11
|
neonDatabaseName: "edge_api",
|
|
12
12
|
localDatabasePort: deriveLocalPostgresPort("edge-api"),
|
|
13
13
|
apiHostname: "api.edge-api.anmho.com",
|
|
14
|
-
modulePath: "
|
|
14
|
+
modulePath: "github.com/anmho/edge-api",
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
|
package/src/naming.ts
CHANGED
|
@@ -94,7 +94,7 @@ export function deriveDefaults(serviceName: string) {
|
|
|
94
94
|
neonDatabaseName: compactDatabaseName(normalizedServiceName),
|
|
95
95
|
localDatabasePort: deriveLocalPostgresPort(normalizedServiceName),
|
|
96
96
|
apiHostname: `api.${normalizedServiceName}.anmho.com`,
|
|
97
|
-
modulePath: `
|
|
97
|
+
modulePath: `github.com/anmho/${normalizedServiceName}`,
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildPostScaffoldCommands } from "./post-scaffold";
|
|
2
|
+
import { buildDeploymentVerificationCommands, buildPostScaffoldCommands } from "./post-scaffold";
|
|
3
3
|
|
|
4
4
|
describe("buildPostScaffoldCommands", () => {
|
|
5
5
|
test("runs create and deploy for HTTP services", () => {
|
|
@@ -17,3 +17,19 @@ describe("buildPostScaffoldCommands", () => {
|
|
|
17
17
|
]);
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
describe("buildDeploymentVerificationCommands", () => {
|
|
22
|
+
test("uses curl health checks for HTTP services", () => {
|
|
23
|
+
expect(buildDeploymentVerificationCommands({ apiHostname: "api.launch.anmho.com", framework: "hono", runtime: "bun" })).toEqual([
|
|
24
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", "https://api.launch.anmho.com/healthz"] },
|
|
25
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", "https://api.launch.anmho.com/readyz"] },
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("uses grpcurl for Go ConnectRPC services", () => {
|
|
30
|
+
expect(buildDeploymentVerificationCommands({ apiHostname: "api.launch.anmho.com", framework: "connectrpc", runtime: "go" })).toContainEqual({
|
|
31
|
+
command: "grpcurl",
|
|
32
|
+
args: ["api.launch.anmho.com:443", "list"],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/post-scaffold.ts
CHANGED
|
@@ -4,6 +4,7 @@ type CommandOptions = {
|
|
|
4
4
|
cwd: string;
|
|
5
5
|
allowFailure?: boolean;
|
|
6
6
|
input?: string;
|
|
7
|
+
quiet?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
type CommandResult = {
|
|
@@ -26,12 +27,32 @@ export async function runPostScaffoldFlow(config: ScaffoldConfig, cwd: string) {
|
|
|
26
27
|
for (const command of buildPostScaffoldCommands(config)) {
|
|
27
28
|
run(command.command, command.args, { cwd });
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
for (const command of buildDeploymentVerificationCommands(config)) {
|
|
31
|
+
run(command.command, command.args, { cwd, quiet: true });
|
|
32
|
+
}
|
|
33
|
+
return { message: "Dependencies installed, service created, service deployed, and production health verified" };
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return { message: "Backend package generated" };
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
export function buildDeploymentVerificationCommands(
|
|
40
|
+
config: Pick<ScaffoldConfig, "apiHostname" | "framework" | "runtime">
|
|
41
|
+
): PostScaffoldCommand[] {
|
|
42
|
+
const origin = `https://${config.apiHostname}`;
|
|
43
|
+
return [
|
|
44
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/healthz`] },
|
|
45
|
+
{ command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/readyz`] },
|
|
46
|
+
...(config.framework === "connectrpc"
|
|
47
|
+
? [
|
|
48
|
+
config.runtime === "go"
|
|
49
|
+
? { command: "grpcurl", args: [`${config.apiHostname}:443`, "list"] }
|
|
50
|
+
: { command: "curl", args: ["--fail", "--show-error", "--silent", `${origin}/debug/connectrpc`] },
|
|
51
|
+
]
|
|
52
|
+
: []),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export function buildPostScaffoldCommands(config: Pick<ScaffoldConfig, "framework">): PostScaffoldCommand[] {
|
|
36
57
|
return [
|
|
37
58
|
...(config.framework === "connectrpc" ? [{ command: "bun", args: ["run", "service", "--", "sdk", "build"] }] : []),
|
|
@@ -56,8 +77,8 @@ function run(command: string, args: string[], options: CommandOptions): CommandR
|
|
|
56
77
|
cwd: options.cwd,
|
|
57
78
|
env: process.env,
|
|
58
79
|
stdin: options.input === undefined ? undefined : encoder.encode(options.input),
|
|
59
|
-
stdout: options.allowFailure ? "pipe" : "inherit",
|
|
60
|
-
stderr: options.allowFailure ? "pipe" : "inherit",
|
|
80
|
+
stdout: options.allowFailure || options.quiet ? "pipe" : "inherit",
|
|
81
|
+
stderr: options.allowFailure || options.quiet ? "pipe" : "inherit",
|
|
61
82
|
});
|
|
62
83
|
|
|
63
84
|
const stdout = result.stdout ? decoder.decode(result.stdout).trim() : "";
|
package/src/scaffold.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ function baseConfig(overrides: Partial<ScaffoldConfig> = {}): ScaffoldConfig {
|
|
|
9
9
|
return {
|
|
10
10
|
directory: "svc",
|
|
11
11
|
serviceName: "dns-api",
|
|
12
|
-
modulePath: "
|
|
12
|
+
modulePath: "github.com/anmho/dns-api",
|
|
13
13
|
target: "cloudrun",
|
|
14
14
|
runtime: "bun",
|
|
15
15
|
framework: "hono",
|
|
@@ -156,11 +156,17 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
156
156
|
|
|
157
157
|
if (variant.runtime === "go") {
|
|
158
158
|
const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
|
|
159
|
-
|
|
160
|
-
expect(goMod).
|
|
159
|
+
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
160
|
+
expect(goMod).toContain("module github.com/anmho/dns-api");
|
|
161
|
+
expect(goMod).not.toContain("module example.com/dns-api");
|
|
162
|
+
expect(packageJson).toContain('"dev": "make dev"');
|
|
163
|
+
expect(packageJson).toContain('"migrate": "make migrate"');
|
|
164
|
+
expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
|
|
165
|
+
expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
|
|
166
|
+
expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
|
|
161
167
|
|
|
162
168
|
const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
|
|
163
|
-
expect(mainGo).toContain("
|
|
169
|
+
expect(mainGo).toContain("github.com/anmho/dns-api");
|
|
164
170
|
if (variant.framework === "connectrpc") {
|
|
165
171
|
expect(goMod).toContain("connectrpc.com/connect");
|
|
166
172
|
expect(mainGo).toContain("NewWaitlistService");
|
|
@@ -202,7 +208,8 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
202
208
|
expect(packageJson).toContain('"auth": "bun run ./scripts/cloudrun/cli.ts auth"');
|
|
203
209
|
expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
|
|
204
210
|
const serviceCli = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).text();
|
|
205
|
-
expect(serviceCli).toContain("service <
|
|
211
|
+
expect(serviceCli).toContain("service <command> [args]");
|
|
212
|
+
expect(serviceCli).toContain("Provision auth, database, migrations, and first deploy");
|
|
206
213
|
expect(serviceCli).toContain("assertServiceNameAvailable(config.serviceName)");
|
|
207
214
|
expect(serviceCli).toContain("ensureAuthResourceServer");
|
|
208
215
|
expect(serviceCli).toContain('["resources", "push", "--path", "./grafana"]');
|
package/src/service.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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, normalizeScaffoldArgs } from "./service";
|
|
5
|
+
import { findGeneratedServiceRoot, generatedDependenciesInstalled, normalizeScaffoldArgs } from "./service";
|
|
6
6
|
|
|
7
7
|
test("normalizeScaffoldArgs treats service create as the scaffold command outside a service repo", () => {
|
|
8
8
|
expect(normalizeScaffoldArgs(["create", "launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
@@ -28,3 +28,14 @@ test("findGeneratedServiceRoot detects generated service context from nested dir
|
|
|
28
28
|
expect(findGeneratedServiceRoot(nested)).toBe(serviceRoot);
|
|
29
29
|
expect(findGeneratedServiceRoot(root)).toBeUndefined();
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("generatedDependenciesInstalled requires node_modules when package.json exists", async () => {
|
|
33
|
+
const root = await mkdtemp(join(tmpdir(), "create-svc-generated-deps-"));
|
|
34
|
+
expect(generatedDependenciesInstalled(root)).toBeTrue();
|
|
35
|
+
|
|
36
|
+
await writeFile(join(root, "package.json"), "{}");
|
|
37
|
+
expect(generatedDependenciesInstalled(root)).toBeFalse();
|
|
38
|
+
|
|
39
|
+
await mkdir(join(root, "node_modules"));
|
|
40
|
+
expect(generatedDependenciesInstalled(root)).toBeTrue();
|
|
41
|
+
});
|
package/src/service.ts
CHANGED
|
@@ -48,6 +48,8 @@ function isGeneratedServiceRoot(path: string) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
51
|
+
ensureGeneratedDependencies(serviceRoot);
|
|
52
|
+
|
|
51
53
|
const cliPath = existsSync(join(serviceRoot, "scripts", "cloudrun", "cli.ts"))
|
|
52
54
|
? "./scripts/cloudrun/cli.ts"
|
|
53
55
|
: "./scripts/workers/cli.ts";
|
|
@@ -63,3 +65,27 @@ function delegateToGeneratedService(serviceRoot: string, argv: string[]) {
|
|
|
63
65
|
process.exit(result.exitCode || 1);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
export function generatedDependenciesInstalled(serviceRoot: string) {
|
|
70
|
+
return !existsSync(join(serviceRoot, "package.json")) || existsSync(join(serviceRoot, "node_modules"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureGeneratedDependencies(serviceRoot: string) {
|
|
74
|
+
if (generatedDependenciesInstalled(serviceRoot)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = Bun.spawnSync(["bun", "install", "--silent"], {
|
|
79
|
+
cwd: serviceRoot,
|
|
80
|
+
env: process.env,
|
|
81
|
+
stdin: "inherit",
|
|
82
|
+
stdout: "pipe",
|
|
83
|
+
stderr: "pipe",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
const output = [result.stdout.toString().trim(), result.stderr.toString().trim()].filter(Boolean).join("\n");
|
|
88
|
+
console.error(["Failed to install generated service dependencies with bun install --silent", output].filter(Boolean).join("\n"));
|
|
89
|
+
process.exit(result.exitCode || 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -27,7 +27,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
27
27
|
const [command, ...rest] = argv;
|
|
28
28
|
|
|
29
29
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log(formatHelp());
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -92,7 +92,26 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
throw new Error(
|
|
95
|
+
throw new Error(`Unknown command: ${command}\n\n${formatHelp()}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatHelp() {
|
|
99
|
+
return [
|
|
100
|
+
"Usage:",
|
|
101
|
+
" service <command> [args]",
|
|
102
|
+
"",
|
|
103
|
+
"Commands:",
|
|
104
|
+
" create Provision auth, database, migrations, and first deploy",
|
|
105
|
+
" deploy Deploy the current service",
|
|
106
|
+
" migrate Apply database migrations",
|
|
107
|
+
" seed Run the seed script when configured",
|
|
108
|
+
" doctor Check local tools and cloud access",
|
|
109
|
+
" auth Manage auth resource server and clients",
|
|
110
|
+
" sdk Build or publish generated SDK artifacts",
|
|
111
|
+
" dns Repair or inspect DNS mappings",
|
|
112
|
+
" dashboards Publish Grafana resources",
|
|
113
|
+
" destroy Remove service-managed cloud resources",
|
|
114
|
+
].join("\n");
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
function runLanguageTask(task: "migrate", env?: Record<string, string | undefined>) {
|
|
@@ -18,7 +18,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
18
18
|
const [command, ...rest] = argv;
|
|
19
19
|
|
|
20
20
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
21
|
-
console.log(
|
|
21
|
+
console.log(formatHelp());
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -87,7 +87,25 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
87
87
|
throw new Error("SDK commands are only available for ConnectRPC services");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
throw new Error(
|
|
90
|
+
throw new Error(`Unknown command: ${command}\n\n${formatHelp()}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatHelp() {
|
|
94
|
+
return [
|
|
95
|
+
"Usage:",
|
|
96
|
+
" service <command> [args]",
|
|
97
|
+
"",
|
|
98
|
+
"Commands:",
|
|
99
|
+
" create Provision auth, database, Hyperdrive, and first deploy",
|
|
100
|
+
" deploy Deploy the Worker",
|
|
101
|
+
" migrate Apply database schema",
|
|
102
|
+
" seed Report seed status",
|
|
103
|
+
" doctor Check local tools and cloud access",
|
|
104
|
+
" auth Manage auth resource server and clients",
|
|
105
|
+
" dns Show Workers custom-domain configuration",
|
|
106
|
+
" dashboards Publish Grafana resources",
|
|
107
|
+
" destroy Remove service-managed Worker resources",
|
|
108
|
+
].join("\n");
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
function run(command: string, args: string[], options: { allowFailure?: boolean; capture?: boolean } = {}) {
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
"service": "./scripts/cloudrun/cli.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
+
"dev": "make dev",
|
|
9
10
|
"service": "bun run ./scripts/cloudrun/cli.ts",
|
|
11
|
+
"migrate": "make migrate",
|
|
12
|
+
"gen": "make gen",
|
|
13
|
+
"lint": "make lint",
|
|
14
|
+
"test": "make test",
|
|
15
|
+
"create": "bun run ./scripts/cloudrun/cli.ts create",
|
|
16
|
+
"deploy": "bun run ./scripts/cloudrun/cli.ts deploy",
|
|
10
17
|
"auth": "bun run ./scripts/cloudrun/cli.ts auth",
|
|
11
|
-
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"
|
|
18
|
+
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards",
|
|
19
|
+
"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"
|
|
12
20
|
},
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@anmho/authctl": "0.1.1",
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
"service": "./scripts/cloudrun/cli.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
+
"dev": "make dev",
|
|
9
10
|
"service": "bun run ./scripts/cloudrun/cli.ts",
|
|
11
|
+
"migrate": "make migrate",
|
|
12
|
+
"gen": "make gen",
|
|
13
|
+
"lint": "make lint",
|
|
14
|
+
"test": "make test",
|
|
15
|
+
"create": "bun run ./scripts/cloudrun/cli.ts create",
|
|
16
|
+
"deploy": "bun run ./scripts/cloudrun/cli.ts deploy",
|
|
10
17
|
"auth": "bun run ./scripts/cloudrun/cli.ts auth",
|
|
11
|
-
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"
|
|
18
|
+
"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards",
|
|
19
|
+
"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"
|
|
12
20
|
},
|
|
13
21
|
"dependencies": {
|
|
14
22
|
"@anmho/authctl": "0.1.1",
|