@anyship/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/package.json +1 -1
- package/src/deploy-directory.mjs +27 -5
- package/src/mcp-server.mjs +4 -2
- package/src/project-manifest.mjs +52 -0
package/README.md
CHANGED
|
@@ -43,12 +43,20 @@ stack, builds if needed, and returns a live URL.
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
anyship deploy --project my-app .
|
|
46
|
+
anyship deploy . # re-deploy — name read from .anyship/project.json
|
|
46
47
|
anyship deploy --project my-app . --dry-run # list what would be uploaded
|
|
47
48
|
anyship status --project my-app --watch # poll until deployed or failed
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
On a successful deploy the CLI records the project name and its server-assigned
|
|
52
|
+
stub (`slug`, `projectId`, `api`) in `<dir>/.anyship/project.json`. Later deploys
|
|
53
|
+
of the same directory reuse that name automatically, so `--project` is optional
|
|
54
|
+
once a directory has been deployed. The `.anyship/` directory is never uploaded
|
|
55
|
+
(it's excluded from the archive); add it to `.gitignore` if you don't want it in
|
|
56
|
+
version control.
|
|
57
|
+
|
|
58
|
+
Options: `--project <name>` (optional when a manifest exists), `--api <url>`,
|
|
59
|
+
`--key <token>`, `--env <json>`, `--dry-run`, `--json`.
|
|
52
60
|
|
|
53
61
|
### `anyship status --project <name>`
|
|
54
62
|
|
package/package.json
CHANGED
package/src/deploy-directory.mjs
CHANGED
|
@@ -5,11 +5,13 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { basename, join, relative, resolve } from "node:path";
|
|
6
6
|
import { zipSync } from "fflate";
|
|
7
7
|
import { readAnyshipConfig } from "./config.mjs";
|
|
8
|
+
import { manifestProjectName, writeProjectManifest } from "./project-manifest.mjs";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_API = "https://drydock.anyship.workers.dev";
|
|
10
11
|
|
|
11
12
|
export const EXCLUDED_DIRS = new Set([
|
|
12
13
|
".git",
|
|
14
|
+
".anyship",
|
|
13
15
|
".codex",
|
|
14
16
|
".agents",
|
|
15
17
|
".wrangler",
|
|
@@ -29,7 +31,8 @@ export function deployUsage(commandName = "anyship deploy") {
|
|
|
29
31
|
return `Usage: ${commandName} --project <name> [path]
|
|
30
32
|
|
|
31
33
|
Options:
|
|
32
|
-
--project <name> anyship project name
|
|
34
|
+
--project <name> anyship project name (optional if the directory has a
|
|
35
|
+
recorded .anyship/project.json from a previous deploy)
|
|
33
36
|
--api <url> control-plane URL (defaults to your MCP connector config, ANYSHIP_API_URL, or ${DEFAULT_API})
|
|
34
37
|
--key <token> anyship API key (defaults to ANYSHIP_API_KEY, ~/.anyship, or the key in your MCP connector config — ~/.codex/config.toml or ~/.claude.json)
|
|
35
38
|
--env <json> JSON object of env vars/secrets to merge for the project
|
|
@@ -82,7 +85,8 @@ export function parseDeployArgs(argv) {
|
|
|
82
85
|
}
|
|
83
86
|
throw new Error(`unknown argument: ${arg}`);
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
// `--project` may be omitted when the target directory already has a recorded
|
|
89
|
+
// .anyship/project.json; deployDirectory resolves it against the manifest.
|
|
86
90
|
return args;
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -275,9 +279,17 @@ export async function buildZip(root) {
|
|
|
275
279
|
|
|
276
280
|
export async function deployDirectory(args, hooks = {}) {
|
|
277
281
|
const root = resolve(args.path ?? ".");
|
|
282
|
+
// Resolve the project name: explicit flag wins, else the name recorded for
|
|
283
|
+
// this directory on a previous deploy (.anyship/project.json).
|
|
284
|
+
const project = (args.project && args.project.trim()) || manifestProjectName(root);
|
|
285
|
+
if (!project) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
"--project is required — pass it once and anyship records it in .anyship/project.json so later deploys of this directory don't need it.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
278
290
|
const { zip, files, total } = await buildZip(root);
|
|
279
291
|
const summary = {
|
|
280
|
-
project
|
|
292
|
+
project,
|
|
281
293
|
root,
|
|
282
294
|
files,
|
|
283
295
|
fileCount: files.length,
|
|
@@ -293,7 +305,7 @@ export async function deployDirectory(args, hooks = {}) {
|
|
|
293
305
|
hooks.onUpload?.({ ...summary, api });
|
|
294
306
|
|
|
295
307
|
const form = new FormData();
|
|
296
|
-
form.append("project",
|
|
308
|
+
form.append("project", project);
|
|
297
309
|
if (args.env) form.append("env", args.env);
|
|
298
310
|
form.append("file", new Blob([zip], { type: "application/zip" }), `${basename(root) || "source"}.zip`);
|
|
299
311
|
|
|
@@ -307,7 +319,17 @@ export async function deployDirectory(args, hooks = {}) {
|
|
|
307
319
|
if (!res.ok) {
|
|
308
320
|
throw new Error(apiErrorMessage(res.status, data, text));
|
|
309
321
|
}
|
|
310
|
-
|
|
322
|
+
// Record this directory → project mapping (name + server-assigned stub/slug)
|
|
323
|
+
// so future deploys of the same folder don't need --project. Best-effort.
|
|
324
|
+
const deployed = data && typeof data === "object" ? data.project : null;
|
|
325
|
+
const manifestPath = writeProjectManifest(root, {
|
|
326
|
+
project,
|
|
327
|
+
projectId: deployed?.id,
|
|
328
|
+
slug: deployed?.slug,
|
|
329
|
+
api,
|
|
330
|
+
lastDeployedAt: new Date().toISOString(),
|
|
331
|
+
});
|
|
332
|
+
return { dryRun: false, ...summary, api, manifestPath, response: data ?? text };
|
|
311
333
|
}
|
|
312
334
|
|
|
313
335
|
export function apiErrorMessage(status, data, text = "") {
|
package/src/mcp-server.mjs
CHANGED
|
@@ -85,8 +85,9 @@ async function remoteProxy(method, params, deps) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
async function localDeploy(args, deps) {
|
|
88
|
+
// `project` may be omitted when the directory has a recorded
|
|
89
|
+
// .anyship/project.json — deployDirectory resolves it against the manifest.
|
|
88
90
|
const project = String(args?.project ?? "").trim();
|
|
89
|
-
if (!project) throw new Error("`project` is required");
|
|
90
91
|
const path = typeof args?.path === "string" && args.path.trim() ? args.path : ".";
|
|
91
92
|
const env =
|
|
92
93
|
args?.env && typeof args.env === "object" && !Array.isArray(args.env)
|
|
@@ -95,10 +96,11 @@ async function localDeploy(args, deps) {
|
|
|
95
96
|
const runDeploy = deps.deployDirectory ?? deployDirectory;
|
|
96
97
|
const result = await runDeploy({ project, path, env }, { interactive: false });
|
|
97
98
|
const root = resolve(path);
|
|
99
|
+
const resolvedProject = result?.project ?? project;
|
|
98
100
|
const message = result?.response?.message ?? "Deploying. Poll deployment_status for progress.";
|
|
99
101
|
const depId = result?.response?.deployment?.id;
|
|
100
102
|
return (
|
|
101
|
-
`🚧 Uploaded ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} from ${root} for "${
|
|
103
|
+
`🚧 Uploaded ${result.fileCount} file${result.fileCount === 1 ? "" : "s"} from ${root} for "${resolvedProject}"` +
|
|
102
104
|
`${depId ? ` (deployment ${depId})` : ""}.\n${message}`
|
|
103
105
|
);
|
|
104
106
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Per-directory deploy record: `<project-root>/.anyship/project.json`. Written
|
|
2
|
+
// on a successful deploy and read on the next one so re-deploying a known folder
|
|
3
|
+
// doesn't need the project name re-supplied — the same (account, name) pair that
|
|
4
|
+
// the control plane keys re-deploys on is remembered locally. The `.anyship`
|
|
5
|
+
// directory is in EXCLUDED_DIRS, so this file is never uploaded in the archive.
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export function manifestDir(root) {
|
|
10
|
+
return join(root, ".anyship");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function manifestPath(root) {
|
|
14
|
+
return join(manifestDir(root), "project.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Returns the stored record, or {} when there's no manifest / it's unreadable.
|
|
18
|
+
export function readProjectManifest(root) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(readFileSync(manifestPath(root), "utf8"));
|
|
21
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The project name recorded for this directory, if any.
|
|
28
|
+
export function manifestProjectName(root) {
|
|
29
|
+
const name = readProjectManifest(root).project;
|
|
30
|
+
return typeof name === "string" && name.trim() ? name.trim() : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Merge-write so fields we don't set (e.g. a url filled in later) survive.
|
|
34
|
+
// Best-effort: a failure to record the manifest must never fail the deploy.
|
|
35
|
+
export function writeProjectManifest(root, patch) {
|
|
36
|
+
try {
|
|
37
|
+
const dir = manifestDir(root);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
const merged = { ...readProjectManifest(root), ...patch };
|
|
40
|
+
for (const key of Object.keys(merged)) {
|
|
41
|
+
if (merged[key] == null) delete merged[key];
|
|
42
|
+
}
|
|
43
|
+
writeFileSync(manifestPath(root), `${JSON.stringify(merged, null, 2)}\n`);
|
|
44
|
+
return manifestPath(root);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function manifestExists(root) {
|
|
51
|
+
return existsSync(manifestPath(root));
|
|
52
|
+
}
|