@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 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
- Options: `--project <name>`, `--api <url>`, `--key <token>`, `--env <json>`,
51
- `--dry-run`, `--json`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyship/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "anyship CLI — deploy a local project directory, run a local MCP server for AI agents, and log in.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- if (!args.help && !args.project) throw new Error("--project is required");
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: args.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", args.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
- return { dryRun: false, ...summary, api, response: data ?? text };
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 = "") {
@@ -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 "${project}"` +
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
+ }