svcloud 0.1.4 → 0.1.5
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 +29 -0
- package/package.json +1 -1
- package/src/commands/bundle.ts +224 -0
- package/src/commands/storage.ts +108 -0
- package/src/index.ts +10 -0
package/README.md
CHANGED
|
@@ -57,7 +57,9 @@ svcloud init Connect a repo (defaults to the one in your workin
|
|
|
57
57
|
svcloud runs <app> Show or watch an app's provisioning run
|
|
58
58
|
svcloud secrets <cmd> Manage an app's keys & settings (list/set/remove)
|
|
59
59
|
svcloud db <cmd> Browse and edit an app's database (see `svcloud db` for subcommands)
|
|
60
|
+
svcloud storage presign Get a direct URL for one file in an app's file storage
|
|
60
61
|
svcloud deploy <app> Push the current branch and watch the build (must be the app's default branch)
|
|
62
|
+
svcloud bundle <cmd> Keep an app's starter current (status/update/done)
|
|
61
63
|
svcloud mcp Run the local MCP bridge (for a coding agent's harness config)
|
|
62
64
|
svcloud mcp setup <harness> Write a coding agent harness's MCP config for svcloud
|
|
63
65
|
svcloud mcp check Diagnose sign-in state, harness configs, and live tool visibility
|
|
@@ -75,6 +77,33 @@ to skip what it would otherwise infer or ask about; `runs` reads `--id
|
|
|
75
77
|
|
|
76
78
|
`logs` and `dev` are still on the way.
|
|
77
79
|
|
|
80
|
+
### Keeping an app's starter current
|
|
81
|
+
|
|
82
|
+
SV Cloud's starter gets better over time, and an app that never catches up
|
|
83
|
+
keeps working but stops being able to do things the platform can now do. It can
|
|
84
|
+
also keep a deploy step that fails for reasons nothing in the repo explains.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
svcloud bundle status <app> # is this app behind?
|
|
88
|
+
svcloud bundle update <app> # stage the update for review
|
|
89
|
+
svcloud bundle done <app> # record it, after you commit
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`update` writes the new versions into `sv-cloud-bundle/` along with an
|
|
93
|
+
`UPDATE.md` explaining what to do with each file. It changes nothing in your
|
|
94
|
+
app's own source, and it does not commit or push — you read the diff, and
|
|
95
|
+
`git` stays the undo. Most people will hand this to their coding agent
|
|
96
|
+
instead: it can read the same plan through SV Cloud's tools, and it knows
|
|
97
|
+
which files it may replace outright and which ones it has to merge into work
|
|
98
|
+
you did.
|
|
99
|
+
|
|
100
|
+
`update` refuses to run with uncommitted changes, so that the diff it produces
|
|
101
|
+
means exactly one thing.
|
|
102
|
+
|
|
103
|
+
Run `svcloud bundle done` once the changes are committed. Skipping it does no
|
|
104
|
+
damage, but the app keeps reporting itself out of date and the next update
|
|
105
|
+
starts from the wrong place.
|
|
106
|
+
|
|
78
107
|
## Connect your coding agent
|
|
79
108
|
|
|
80
109
|
`svcloud mcp` runs a local MCP server that your coding agent — Antigravity,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svcloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "The SV Cloud CLI. Alpha: login, logout, status, open, projects list, mcp, init, and runs are built; see PLANNING.md for what's still missing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `svcloud bundle` — PLANNING.md §4 Step 8, apps/cloud F27. Brings a connected
|
|
3
|
+
* repo up to a newer starter template.
|
|
4
|
+
*
|
|
5
|
+
* The merge rules are the platform's, not the CLI's: `GET /bundle/plan`
|
|
6
|
+
* returns the base version's bytes, the new version's bytes, and a per-file
|
|
7
|
+
* action, and everything here is staging and presentation. That is deliberate,
|
|
8
|
+
* for the same reason the MCP bridge holds no tool registry — a rule that
|
|
9
|
+
* lives on the server cannot drift against the version of the CLI somebody
|
|
10
|
+
* happens to have installed.
|
|
11
|
+
*
|
|
12
|
+
* `update` writes into `sv-cloud-bundle/` and stops. It does not `git add`,
|
|
13
|
+
* commit, or push, and it never touches `src/`: the owner reads a diff, and
|
|
14
|
+
* git stays the undo. `done` is a separate command precisely because it has to
|
|
15
|
+
* run AFTER a human commits — folding it into `update` would mean recording
|
|
16
|
+
* that the update landed before it had.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { apiFetch } from "../lib/api";
|
|
21
|
+
import { findProjectBySlug } from "../lib/find-project";
|
|
22
|
+
import { gitWorkingState } from "../lib/git";
|
|
23
|
+
import { die, printJson, printTable } from "../lib/output";
|
|
24
|
+
|
|
25
|
+
const STAGE_DIR = "sv-cloud-bundle";
|
|
26
|
+
|
|
27
|
+
interface BundleStatus {
|
|
28
|
+
project_id: string;
|
|
29
|
+
current_version: string | null;
|
|
30
|
+
latest_version: string;
|
|
31
|
+
update_available: boolean;
|
|
32
|
+
legacy: boolean;
|
|
33
|
+
base_unavailable_reason: string | null;
|
|
34
|
+
workflow_stale: boolean;
|
|
35
|
+
workflow_warning: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PlannedFile {
|
|
39
|
+
src: string;
|
|
40
|
+
path: string;
|
|
41
|
+
class: string;
|
|
42
|
+
action: string;
|
|
43
|
+
target?: string;
|
|
44
|
+
base?: string;
|
|
45
|
+
guidance: string;
|
|
46
|
+
retirement?: { src: string; replaced_by?: string | null; note: string };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BundlePlan extends BundleStatus {
|
|
50
|
+
framework: string;
|
|
51
|
+
migrate_once: boolean;
|
|
52
|
+
files: PlannedFile[];
|
|
53
|
+
migration_notes: string | null;
|
|
54
|
+
manifest_merge: unknown;
|
|
55
|
+
instructions: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Human-facing one-liners. The wire's `action` stays machine-readable. */
|
|
59
|
+
const ACTION_LABEL: Record<string, string> = {
|
|
60
|
+
skip: "unchanged",
|
|
61
|
+
"fast-forward": "replace",
|
|
62
|
+
conflict: "merge (you edited it)",
|
|
63
|
+
create: "new file",
|
|
64
|
+
retire: "delete",
|
|
65
|
+
"retire-conflict": "retired, you use it",
|
|
66
|
+
locate: "find it first",
|
|
67
|
+
"merge-manifest": "merge fields",
|
|
68
|
+
"report-only": "yours, unchanged",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
async function resolve(slug: string | undefined): Promise<{ id: string; name: string }> {
|
|
72
|
+
if (!slug) die("Usage: svcloud bundle <status|update|done> <app>");
|
|
73
|
+
const project = await findProjectBySlug(slug);
|
|
74
|
+
if (!project) die(`No app named "${slug}".`);
|
|
75
|
+
return { id: project.id, name: project.name };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function statusCmd(slug: string | undefined, json: boolean): Promise<void> {
|
|
79
|
+
const project = await resolve(slug);
|
|
80
|
+
const status = await apiFetch<BundleStatus>(`/api/v1/projects/${project.id}/bundle`);
|
|
81
|
+
if (json) {
|
|
82
|
+
printJson(status);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(`App: ${project.name}`);
|
|
86
|
+
console.log(`Starter: ${status.current_version ?? "not recorded"}`);
|
|
87
|
+
console.log(`Newest: ${status.latest_version}`);
|
|
88
|
+
if (status.workflow_warning) console.log(`\n! ${status.workflow_warning}`);
|
|
89
|
+
if (!status.update_available) {
|
|
90
|
+
console.log("\nUp to date.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (status.legacy) {
|
|
94
|
+
console.log(
|
|
95
|
+
"\nThis app was set up before starter versions were recorded, so the first update " +
|
|
96
|
+
"has no starting point to compare against. Review every change it proposes.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
console.log("\nAn update is available. Run `svcloud bundle update " + (slug ?? "") + "`.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function updateCmd(slug: string | undefined, json: boolean): Promise<void> {
|
|
103
|
+
const project = await resolve(slug);
|
|
104
|
+
|
|
105
|
+
// Same preconditions as `deploy`, for the same reason: this writes files
|
|
106
|
+
// into the working tree, and a dirty tree makes the resulting diff
|
|
107
|
+
// impossible for the owner to read as "what the update did".
|
|
108
|
+
const state = await gitWorkingState();
|
|
109
|
+
if (!state) {
|
|
110
|
+
die("Run this from the app's local git repository (no repo found, or `git` isn't on PATH).");
|
|
111
|
+
}
|
|
112
|
+
if (state.dirty) {
|
|
113
|
+
die(
|
|
114
|
+
"Working tree has uncommitted changes. Commit or stash them first, so the update's own " +
|
|
115
|
+
"changes are the only thing in the diff.",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const plan = await apiFetch<BundlePlan>(`/api/v1/projects/${project.id}/bundle/plan`);
|
|
120
|
+
if (json) {
|
|
121
|
+
printJson(plan);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!plan.update_available) {
|
|
126
|
+
console.log(`${project.name} is already on the newest starter (${plan.latest_version}).`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const actionable = plan.files.filter((f) => f.action !== "skip");
|
|
131
|
+
const cwd = process.cwd();
|
|
132
|
+
const written: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (const f of actionable) {
|
|
135
|
+
if (f.target === undefined) continue;
|
|
136
|
+
const staged = join(cwd, STAGE_DIR, "files", f.src);
|
|
137
|
+
await mkdir(dirname(staged), { recursive: true });
|
|
138
|
+
await writeFile(staged, f.target, "utf8");
|
|
139
|
+
written.push(f.src);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const notes = [
|
|
143
|
+
`# Starter update: ${plan.current_version ?? "unrecorded"} to ${plan.latest_version}`,
|
|
144
|
+
"",
|
|
145
|
+
plan.instructions,
|
|
146
|
+
"",
|
|
147
|
+
plan.base_unavailable_reason ? `**Note.** ${plan.base_unavailable_reason}\n` : "",
|
|
148
|
+
"## Files",
|
|
149
|
+
"",
|
|
150
|
+
...actionable.map((f) => {
|
|
151
|
+
const head = `### ${f.path} — ${ACTION_LABEL[f.action] ?? f.action}`;
|
|
152
|
+
const staged =
|
|
153
|
+
f.target === undefined ? "" : `\nNew version staged at \`${STAGE_DIR}/files/${f.src}\`.`;
|
|
154
|
+
const retired = f.retirement
|
|
155
|
+
? `\nRetired${f.retirement.replaced_by ? `, superseded by ${f.retirement.replaced_by}` : ""}. ${f.retirement.note}`
|
|
156
|
+
: "";
|
|
157
|
+
return `${head}\n\n${f.guidance}${staged}${retired}\n`;
|
|
158
|
+
}),
|
|
159
|
+
plan.migration_notes ? `## Migration notes\n\n${plan.migration_notes}\n` : "",
|
|
160
|
+
].join("\n");
|
|
161
|
+
|
|
162
|
+
await writeFile(join(cwd, STAGE_DIR, "UPDATE.md"), notes, "utf8");
|
|
163
|
+
|
|
164
|
+
printTable(
|
|
165
|
+
actionable.map((f) => ({
|
|
166
|
+
file: f.path,
|
|
167
|
+
what: ACTION_LABEL[f.action] ?? f.action,
|
|
168
|
+
})),
|
|
169
|
+
["file", "what"],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
console.log(`\nStaged ${written.length} file(s) under ${STAGE_DIR}/ — nothing in src/ changed.`);
|
|
173
|
+
console.log(`Read ${STAGE_DIR}/UPDATE.md, or open this repo with your coding agent and ask it`);
|
|
174
|
+
console.log("to apply the starter update; it can read the same plan through SV Cloud's tools.");
|
|
175
|
+
console.log(`\nWhen the changes are committed, run \`svcloud bundle done ${slug ?? ""}\`.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function doneCmd(slug: string | undefined, json: boolean): Promise<void> {
|
|
179
|
+
const project = await resolve(slug);
|
|
180
|
+
const result = await apiFetch<{
|
|
181
|
+
version: string;
|
|
182
|
+
marker_path: string;
|
|
183
|
+
marker_contents: string;
|
|
184
|
+
changed: boolean;
|
|
185
|
+
}>(`/api/v1/projects/${project.id}/bundle/applied`, { method: "POST", body: {} });
|
|
186
|
+
|
|
187
|
+
// The marker is written HERE rather than by the server, because the server
|
|
188
|
+
// does not touch repos — same rule the rest of this feature follows.
|
|
189
|
+
const target = join(process.cwd(), result.marker_path);
|
|
190
|
+
await mkdir(dirname(target), { recursive: true });
|
|
191
|
+
await writeFile(target, result.marker_contents, "utf8");
|
|
192
|
+
|
|
193
|
+
if (json) {
|
|
194
|
+
printJson(result);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.log(`Recorded ${project.name} as running starter ${result.version}.`);
|
|
198
|
+
console.log(`Wrote ${result.marker_path}. Commit it — it is how the next update knows where to`);
|
|
199
|
+
console.log("start, and an app whose marker disagrees with its files updates badly.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function bundleCommand(argv: string[], json: boolean): Promise<void> {
|
|
203
|
+
const [sub, ...rest] = argv;
|
|
204
|
+
switch (sub) {
|
|
205
|
+
case "status":
|
|
206
|
+
await statusCmd(rest[0], json);
|
|
207
|
+
return;
|
|
208
|
+
case "update":
|
|
209
|
+
await updateCmd(rest[0], json);
|
|
210
|
+
return;
|
|
211
|
+
case "done":
|
|
212
|
+
await doneCmd(rest[0], json);
|
|
213
|
+
return;
|
|
214
|
+
default:
|
|
215
|
+
die(
|
|
216
|
+
[
|
|
217
|
+
"Usage:",
|
|
218
|
+
" svcloud bundle status <app> Is this app running an out-of-date starter?",
|
|
219
|
+
" svcloud bundle update <app> Stage the update under sv-cloud-bundle/ for review",
|
|
220
|
+
" svcloud bundle done <app> Record the update after you have committed it",
|
|
221
|
+
].join("\n"),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `svcloud storage` — direct URLs into an app's file storage.
|
|
3
|
+
*
|
|
4
|
+
* Only presigning, deliberately. `svcloud storage put` and `get` would put the
|
|
5
|
+
* CLI in the data path for a file that may be gigabytes, which is the exact
|
|
6
|
+
* cost presigned URLs exist to avoid; the URL this hands back works with curl,
|
|
7
|
+
* a browser, or anything else, and moves bytes straight between the caller and
|
|
8
|
+
* storage.
|
|
9
|
+
*
|
|
10
|
+
* A GET URL is a shareable download link for as long as it lives. The server
|
|
11
|
+
* caps that lifetime and reports what it actually granted, which may be less
|
|
12
|
+
* than was asked for — so `expires_in` in the output is the truth, not the
|
|
13
|
+
* `--expires-in` flag.
|
|
14
|
+
*/
|
|
15
|
+
import { statSync } from "node:fs";
|
|
16
|
+
import { apiFetch } from "../lib/api";
|
|
17
|
+
import { findProjectBySlug } from "../lib/find-project";
|
|
18
|
+
import { die, printJson } from "../lib/output";
|
|
19
|
+
|
|
20
|
+
const USAGE = `Usage:
|
|
21
|
+
svcloud storage presign <app> <key> [--method GET|PUT] [--expires-in <seconds>]
|
|
22
|
+
[--size <bytes> | --file <path>] [--content-type <type>]
|
|
23
|
+
|
|
24
|
+
--method GET (default) for a download or share link, PUT to upload.
|
|
25
|
+
--size Required for PUT. Signed into the URL, so an upload of any
|
|
26
|
+
other size is refused.
|
|
27
|
+
--file Read the size from a local file instead of passing --size.
|
|
28
|
+
--expires-in Seconds. Capped by the server; the output reports what was granted.`;
|
|
29
|
+
|
|
30
|
+
interface PresignResponse {
|
|
31
|
+
project_id: string;
|
|
32
|
+
key: string;
|
|
33
|
+
method: string;
|
|
34
|
+
url: string;
|
|
35
|
+
expires_in: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flagValue(argv: string[], name: string): string | undefined {
|
|
39
|
+
const index = argv.indexOf(name);
|
|
40
|
+
if (index === -1) return undefined;
|
|
41
|
+
const value = argv[index + 1];
|
|
42
|
+
if (value === undefined || value.startsWith("--")) die(`${name} needs a value.\n\n${USAGE}`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function storageCommand(argv: string[], json: boolean): Promise<void> {
|
|
47
|
+
const [subcommand, ...rest] = argv;
|
|
48
|
+
if (subcommand !== "presign") die(USAGE);
|
|
49
|
+
|
|
50
|
+
const [slug, key] = rest;
|
|
51
|
+
if (!slug || !key) die(USAGE);
|
|
52
|
+
|
|
53
|
+
const project = await findProjectBySlug(slug);
|
|
54
|
+
if (!project) die(`No app named "${slug}".`);
|
|
55
|
+
|
|
56
|
+
const method = (flagValue(rest, "--method") ?? "GET").toUpperCase();
|
|
57
|
+
if (method !== "GET" && method !== "PUT") die(`--method must be GET or PUT.\n\n${USAGE}`);
|
|
58
|
+
|
|
59
|
+
const body: Record<string, unknown> = { key, method };
|
|
60
|
+
|
|
61
|
+
const expiresIn = flagValue(rest, "--expires-in");
|
|
62
|
+
if (expiresIn !== undefined) {
|
|
63
|
+
const seconds = Number(expiresIn);
|
|
64
|
+
if (!Number.isInteger(seconds) || seconds <= 0) die("--expires-in must be a whole number of seconds.");
|
|
65
|
+
body.expires_in = seconds;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const contentType = flagValue(rest, "--content-type");
|
|
69
|
+
if (contentType) body.content_type = contentType;
|
|
70
|
+
|
|
71
|
+
if (method === "PUT") {
|
|
72
|
+
const size = flagValue(rest, "--size");
|
|
73
|
+
const file = flagValue(rest, "--file");
|
|
74
|
+
if (size !== undefined && file !== undefined) die("Pass --size or --file, not both.");
|
|
75
|
+
if (size !== undefined) {
|
|
76
|
+
const bytes = Number(size);
|
|
77
|
+
if (!Number.isInteger(bytes) || bytes < 0) die("--size must be a whole number of bytes.");
|
|
78
|
+
body.content_length = bytes;
|
|
79
|
+
} else if (file !== undefined) {
|
|
80
|
+
try {
|
|
81
|
+
body.content_length = statSync(file).size;
|
|
82
|
+
} catch {
|
|
83
|
+
die(`Could not read "${file}".`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
die(`A PUT needs --size or --file: the size is signed into the URL.\n\n${USAGE}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await apiFetch<PresignResponse>(
|
|
91
|
+
`/api/v1/projects/${project.id}/storage/presign`,
|
|
92
|
+
{ method: "POST", body: JSON.stringify(body) },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (json) {
|
|
96
|
+
printJson(result);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(result.url);
|
|
101
|
+
console.error("");
|
|
102
|
+
console.error(`${result.method} ${result.key} — valid for ${result.expires_in}s.`);
|
|
103
|
+
if (result.method === "GET") {
|
|
104
|
+
console.error("Anybody with this link can download the file until it expires.");
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`Upload with: curl -X PUT --upload-file <file> "<url>"`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* few that need subcommands (`secrets`, `db`, `mcp`) do their own
|
|
6
6
|
* second-level switch — no subcommand ambiguity worth a parser library yet.
|
|
7
7
|
*/
|
|
8
|
+
import { bundleCommand } from "./commands/bundle";
|
|
8
9
|
import { dbCommand } from "./commands/db";
|
|
9
10
|
import { deployCommand } from "./commands/deploy";
|
|
10
11
|
import { initCommand } from "./commands/init";
|
|
@@ -15,6 +16,7 @@ import { openCommand } from "./commands/open";
|
|
|
15
16
|
import { projectsCommand } from "./commands/projects";
|
|
16
17
|
import { runsCommand } from "./commands/runs";
|
|
17
18
|
import { secretsCommand } from "./commands/secrets";
|
|
19
|
+
import { storageCommand } from "./commands/storage";
|
|
18
20
|
import { statusCommand } from "./commands/status";
|
|
19
21
|
import { whoamiCommand } from "./commands/whoami";
|
|
20
22
|
import { AuthRequiredError, ApiError } from "./lib/api";
|
|
@@ -33,8 +35,10 @@ Usage:
|
|
|
33
35
|
svcloud init Connect a local repo as a new app
|
|
34
36
|
svcloud runs <app> Show or watch an app's provisioning run
|
|
35
37
|
svcloud secrets <cmd> Manage an app's keys & settings (list/set/remove)
|
|
38
|
+
svcloud storage presign Get a direct URL for one file in an app's storage
|
|
36
39
|
svcloud db <cmd> Browse and edit an app's database (see 'svcloud db' for subcommands)
|
|
37
40
|
svcloud deploy <app> Push the current branch and watch the build (must be the app's default branch)
|
|
41
|
+
svcloud bundle <cmd> Keep an app's starter current (status/update/done)
|
|
38
42
|
svcloud mcp Run the local MCP bridge (for a coding agent's harness config)
|
|
39
43
|
svcloud mcp setup <harness> Write a coding agent harness's MCP config for svcloud
|
|
40
44
|
svcloud mcp check Diagnose sign-in state, harness configs, and live tool visibility
|
|
@@ -69,6 +73,9 @@ async function main(): Promise<void> {
|
|
|
69
73
|
case "logout":
|
|
70
74
|
await logoutCommand();
|
|
71
75
|
return;
|
|
76
|
+
case "bundle":
|
|
77
|
+
await bundleCommand(rest, json);
|
|
78
|
+
return;
|
|
72
79
|
case "whoami":
|
|
73
80
|
await whoamiCommand(json);
|
|
74
81
|
return;
|
|
@@ -93,6 +100,9 @@ async function main(): Promise<void> {
|
|
|
93
100
|
case "secrets":
|
|
94
101
|
await secretsCommand(rest, json);
|
|
95
102
|
return;
|
|
103
|
+
case "storage":
|
|
104
|
+
await storageCommand(rest, json);
|
|
105
|
+
return;
|
|
96
106
|
case "db":
|
|
97
107
|
await dbCommand(rest, json);
|
|
98
108
|
return;
|