@abyssinia-labs/sokie 0.7.0-staging.138 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/sokie.mjs +332 -159
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -90,6 +90,17 @@ sokie requests # what waits for your answer, and what you ask
90
90
  sokie requests approve kd7a3f9c # or decline, with --note
91
91
  ```
92
92
 
93
+ An owner or admin can also make a server's production deploys wait for one
94
+ of them. A deploy that must wait is held: `sokie deploy` stops there and
95
+ says why, and it serves only the team, signed in, at its own address for up
96
+ to 7 days. It's listed in `sokie requests` for an owner or admin to approve,
97
+ which puts it live:
98
+
99
+ ```bash
100
+ sokie call getInvoice --held 12 # try held deploy 12 before anyone approves it
101
+ sokie deploys # held shows as waiting for an admin
102
+ ```
103
+
93
104
  ## Links
94
105
 
95
106
  - [Docs](https://sokie.sh/docs)
package/dist/sokie.mjs CHANGED
@@ -24033,9 +24033,9 @@ var require___dev__ = __commonJS((exports, module) => {
24033
24033
 
24034
24034
  // package.json
24035
24035
  var name = "@abyssinia-labs/sokie";
24036
- var version = "0.7.0-staging.138";
24036
+ var version = "0.8.0";
24037
24037
  var sokie = {
24038
- site: "https://staging.sokie.sh"
24038
+ site: "https://sokie.sh"
24039
24039
  };
24040
24040
 
24041
24041
  // src/command.ts
@@ -44961,6 +44961,7 @@ function utf8Bytes(text3) {
44961
44961
  // src/deploy.ts
44962
44962
  var POLL_MS = 2000;
44963
44963
  var WAIT_MS = 5 * 60 * 1000;
44964
+ var DONE = new Set(["live", "failed", "held"]);
44964
44965
 
44965
44966
  class DeployError extends Error {
44966
44967
  code;
@@ -45023,7 +45024,7 @@ async function followDeploy(platform, deploymentId, options) {
45023
45024
  last = found.status;
45024
45025
  await options.progress?.(found.status);
45025
45026
  }
45026
- if (!options.wait || found.status === "live" || found.status === "failed")
45027
+ if (!options.wait || DONE.has(found.status))
45027
45028
  return found;
45028
45029
  if (Date.now() > deadline) {
45029
45030
  throw new DeployError("deploy_timeout", "The deploy is still running; check it in the app.");
@@ -47368,12 +47369,26 @@ function routeCall(definition, name2, input2) {
47368
47369
  }
47369
47370
 
47370
47371
  // src/call-target.ts
47371
- function targetFrom(preview, production) {
47372
- if (preview !== undefined && production) {
47373
- throw new UsageError("Say --preview <n> or --production, not both.");
47372
+ function targetFrom({ preview, production = false, held }) {
47373
+ const given = [
47374
+ preview === undefined ? "" : "--preview <n>",
47375
+ held === undefined ? "" : "--held <n>",
47376
+ production ? "--production" : ""
47377
+ ].filter(Boolean);
47378
+ if (given.length > 1) {
47379
+ const last = given.pop();
47380
+ const all = given.length === 1 ? "both" : "all three";
47381
+ throw new UsageError(`Say ${given.join(", ")} or ${last}, not ${all}.`);
47374
47382
  }
47375
47383
  if (production)
47376
47384
  return { kind: "production" };
47385
+ if (held !== undefined) {
47386
+ const number5 = Number(held.replace(/^#/, ""));
47387
+ if (!Number.isInteger(number5) || number5 < 1) {
47388
+ throw new UsageError(`--held takes a deploy's number, like 12, not ${held}.`);
47389
+ }
47390
+ return { kind: "held", number: number5 };
47391
+ }
47377
47392
  if (preview === undefined)
47378
47393
  return { kind: "dev" };
47379
47394
  const number4 = Number(preview);
@@ -47385,6 +47400,8 @@ function targetFrom(preview, production) {
47385
47400
  function targetName(target) {
47386
47401
  if (target.kind === "dev")
47387
47402
  return "sokie dev";
47403
+ if (target.kind === "held")
47404
+ return `held deploy #${target.number}`;
47388
47405
  return target.kind === "production" ? "production" : `preview #${target.number}`;
47389
47406
  }
47390
47407
  function refusal2(code2, server, target) {
@@ -47395,6 +47412,8 @@ function refusal2(code2, server, target) {
47395
47412
  return new DeployError("not_live", `${server.slug} has nothing live yet, so there's nothing to call. sokie deploy puts it live.`);
47396
47413
  case "NO_PREVIEW":
47397
47414
  return new DeployError("no_preview", `${server.slug} has no live preview for #${target.kind === "preview" ? target.number : "?"}.`);
47415
+ case "NOT_HELD":
47416
+ return new DeployError("not_held", `Deploy #${target.kind === "held" ? target.number : "?"} of ${server.slug} isn't held now: it went live, or was declined, withdrawn, replaced or lapsed. sokie deploys says which.`);
47398
47417
  case "PAUSED":
47399
47418
  return new DeployError("paused", `${server.slug} is paused, so it takes no calls.`);
47400
47419
  case "RATE_LIMITED":
@@ -47411,7 +47430,8 @@ async function mintEndpoint(client, server, target) {
47411
47430
  try {
47412
47431
  const minted = await client.action(fn.mintTestToken, {
47413
47432
  serverId: server.serverId,
47414
- ...target.kind === "preview" ? { preview: target.number } : {}
47433
+ ...target.kind === "preview" ? { preview: target.number } : {},
47434
+ ...target.kind === "held" ? { held: target.number } : {}
47415
47435
  });
47416
47436
  return { url: minted.url, token: minted.token, expiresAt: minted.expiresAt };
47417
47437
  } catch (error62) {
@@ -64638,6 +64658,7 @@ async function runCall(argv, out, deps) {
64638
64658
  dir: { type: "string" },
64639
64659
  server: { type: "string" },
64640
64660
  preview: { type: "string" },
64661
+ held: { type: "string" },
64641
64662
  production: { type: "boolean", default: false }
64642
64663
  });
64643
64664
  const [name2, ...extra] = positionals;
@@ -64645,9 +64666,9 @@ async function runCall(argv, out, deps) {
64645
64666
  throw new UsageError(`Give one tool's name: sokie call getTask --input '{"taskId":"t_1"}'.`);
64646
64667
  }
64647
64668
  const input2 = parseInput(values.input);
64648
- const target = targetFrom(values.preview, values.production);
64669
+ const target = targetFrom(values);
64649
64670
  if (target.kind === "dev" && values.server !== undefined) {
64650
- throw new UsageError("--server picks a deploy: add --preview <n> or --production.");
64671
+ throw new UsageError("--server picks a deploy: add --preview <n>, --held <n> or --production.");
64651
64672
  }
64652
64673
  const root = resolve(values.dir ?? ".");
64653
64674
  const local = localRoute(root, name2, input2);
@@ -64687,14 +64708,15 @@ async function runCall(argv, out, deps) {
64687
64708
  }
64688
64709
  var callCommand = {
64689
64710
  name: "call",
64690
- synopsis: "sokie call <tool> [--input '<json>'] [--yes] [--preview <n> | --production] [--server <slug>] [--dir <dir>] [--site <url>]",
64711
+ synopsis: "sokie call <tool> [--input '<json>'] [--yes] [--preview <n> | --held <n> | --production] [--server <slug>] [--dir <dir>] [--site <url>]",
64691
64712
  summary: "Call one tool as an agent would, on sokie dev or a deploy.",
64692
64713
  help: `Calls the tool over MCP and prints what an agent would get back, then how
64693
64714
  long it took. Exits 1 when the call fails.
64694
64715
 
64695
64716
  By default the call goes to the sokie dev running for this checkout (start
64696
64717
  it in another terminal first). --preview <n> calls the pull request's
64697
- preview and --production the live deploy, as you, with a token that lasts
64718
+ preview, --held <n> a deploy waiting for an owner or admin at its own
64719
+ address, and --production the live deploy, as you, with a token that lasts
64698
64720
  five minutes; the call is logged with the client "Sokie test".
64699
64721
 
64700
64722
  A tool that does more than read (write, delete or external) asks first,
@@ -64710,6 +64732,7 @@ called by name too, to see what an agent finds.
64710
64732
  --input <json> The tool's arguments, as one JSON object (default {}).
64711
64733
  --yes, -y Don't ask. Needed when there is no terminal, as in a script.
64712
64734
  --preview <n> Call pull request n's preview deploy.
64735
+ --held <n> Call deploy n while it's held: sokie deploys lists it.
64713
64736
  --production Call the live deploy.
64714
64737
  --server <s> A server by slug, instead of the one this repository is linked to.
64715
64738
  --dir <dir> The repository (default .).
@@ -64847,6 +64870,167 @@ async function exchangeDeployKey(info2, key, fetchFn = fetch) {
64847
64870
  return body.access_token;
64848
64871
  }
64849
64872
 
64873
+ // src/deploy-list.ts
64874
+ var ENDED_HOLDS = {
64875
+ HOLD_DECLINED: "declined",
64876
+ HOLD_WITHDRAWN: "withdrawn",
64877
+ HOLD_EXPIRED: "lapsed"
64878
+ };
64879
+ function statusOf(deploy) {
64880
+ if (deploy.status === "held")
64881
+ return { text: "held", tone: "warn" };
64882
+ if (deploy.status === "failed") {
64883
+ const ended = ENDED_HOLDS[deploy.error?.code ?? ""];
64884
+ return ended ? { text: ended, tone: "dim" } : { text: "failed", tone: "fail" };
64885
+ }
64886
+ if (deploy.status === "queued" || deploy.status === "building") {
64887
+ return { text: deploy.status, tone: "warn" };
64888
+ }
64889
+ if (deploy.previewPr !== undefined)
64890
+ return { text: "preview", tone: deploy.status === "live" ? "ok" : undefined };
64891
+ return deploy.status === "live" ? { text: "live", tone: "ok" } : { text: "replaced", tone: "dim" };
64892
+ }
64893
+ function sourceOf(deploy) {
64894
+ if (deploy.restoreOf !== undefined)
64895
+ return `restore of #${deploy.restoreOf}`;
64896
+ const source = deploy.source;
64897
+ if (!source)
64898
+ return "uploaded from the CLI";
64899
+ if (source.kind === "upload") {
64900
+ const edited = source.edited ?? [];
64901
+ if (edited.length > 0) {
64902
+ const named = edited.length <= 2 ? edited.join(" and ") : `${edited[0]} and ${edited.length - 1} more`;
64903
+ return `${named} edited in the app`;
64904
+ }
64905
+ return source.from === "web" ? "made in Sokie from a spec" : "uploaded from the CLI";
64906
+ }
64907
+ const pr = deploy.previewPr ?? source.prNumber;
64908
+ const sha = source.sha.slice(0, 7);
64909
+ return pr === undefined ? `${sha} on ${source.branch}` : `${sha} · preview #${pr}`;
64910
+ }
64911
+ var DAY_FORMAT = new Intl.DateTimeFormat("en", {
64912
+ month: "short",
64913
+ day: "numeric",
64914
+ timeZone: "UTC"
64915
+ });
64916
+ function onDay(at2) {
64917
+ return DAY_FORMAT.format(at2);
64918
+ }
64919
+ var MINUTE = 60000;
64920
+ var HOUR = 60 * MINUTE;
64921
+ var DAY = 24 * HOUR;
64922
+ function ago(at2, now) {
64923
+ const gone = Math.max(0, now - at2);
64924
+ if (gone < MINUTE)
64925
+ return "just now";
64926
+ if (gone < HOUR)
64927
+ return `${Math.floor(gone / MINUTE)} min ago`;
64928
+ if (gone < DAY)
64929
+ return `${Math.floor(gone / HOUR)} h ago`;
64930
+ if (gone < 2 * DAY)
64931
+ return "yesterday";
64932
+ if (gone < 30 * DAY)
64933
+ return `${Math.floor(gone / DAY)} days ago`;
64934
+ return new Date(at2).toLocaleDateString("en-US", {
64935
+ month: "short",
64936
+ day: "numeric",
64937
+ timeZone: "UTC"
64938
+ });
64939
+ }
64940
+ function headerLine(slug, facts) {
64941
+ if (facts.pin) {
64942
+ const since = facts.pin.skippedPushes === 1 ? "1 push" : `${facts.pin.skippedPushes} pushes`;
64943
+ return {
64944
+ text: `${slug} · production stays on #${facts.live ?? "?"} until sokie deploys resume (${since} since the restore)`,
64945
+ tone: "warn"
64946
+ };
64947
+ }
64948
+ if (facts.branch)
64949
+ return { text: `${slug} · production follows ${facts.branch}`, tone: "dim" };
64950
+ return { text: `${slug} · Sokie keeps its files`, tone: "dim" };
64951
+ }
64952
+ function rowLine(deploy, now) {
64953
+ const status = statusOf(deploy);
64954
+ const number4 = deploy.number === undefined ? "—" : `#${deploy.number}`;
64955
+ const text5 = ` ${number4.padEnd(5)} ${status.text.padEnd(9)} ${sourceOf(deploy).padEnd(32)} ${ago(deploy.createdAt, now)}`;
64956
+ return { text: text5, tone: status.tone === "dim" ? undefined : status.tone };
64957
+ }
64958
+ function heldLine(deploy) {
64959
+ const until = deploy.hold ? ` until ${onDay(deploy.hold.until)}` : "";
64960
+ const where = deploy.heldUrl ? ` · ${deploy.heldUrl}` : "";
64961
+ return { text: ` waiting for an admin${until}${where}`, tone: "dim" };
64962
+ }
64963
+ function listLines2(slug, deploys, facts) {
64964
+ const live = deploys.find((d) => d.status === "live" && d.previewPr === undefined)?.number;
64965
+ const lines = [headerLine(slug, { ...facts, live }), { text: "" }];
64966
+ if (deploys.length === 0) {
64967
+ lines.push({ text: " No deploys yet. sokie deploy makes the first one.", tone: "dim" });
64968
+ return lines;
64969
+ }
64970
+ for (const deploy of deploys) {
64971
+ lines.push(rowLine(deploy, facts.now));
64972
+ if (deploy.status === "held")
64973
+ lines.push(heldLine(deploy));
64974
+ }
64975
+ if (facts.more) {
64976
+ lines.push({
64977
+ text: ` … older ones too. sokie deploys --limit ${facts.limit * 2} goes further back; --json for scripts.`,
64978
+ tone: "dim"
64979
+ });
64980
+ }
64981
+ return lines;
64982
+ }
64983
+ function diffTone(line, index) {
64984
+ if (index === 0)
64985
+ return "strong";
64986
+ const sign = line.trimStart().slice(0, 2);
64987
+ if (sign === "+ ")
64988
+ return "ok";
64989
+ if (sign === "- ")
64990
+ return "fail";
64991
+ if (sign === "↑ " || sign === "↓ ")
64992
+ return "warn";
64993
+ return;
64994
+ }
64995
+
64996
+ // src/deploy-held.ts
64997
+ function names(list) {
64998
+ return list.length > 1 ? `${list.slice(0, -1).join(", ")} and ${list.at(-1)}` : list[0] ?? "";
64999
+ }
65000
+ function holdWhy(deploy, slug) {
65001
+ const tools = deploy.hold?.tools ?? [];
65002
+ if (deploy.hold?.reason === "newTools" && tools.length > 0) {
65003
+ const verbs = tools.length === 1 ? "deletes or acts" : "delete or act";
65004
+ return `It adds ${names(tools)}, which ${verbs} outside the API, so an owner or admin looks first.`;
65005
+ }
65006
+ return `Production deploys of ${slug} wait for an owner or admin.`;
65007
+ }
65008
+ function heldLines(slug, deploy) {
65009
+ const which = deploy.number === undefined ? "this deploy" : `deploy #${deploy.number}`;
65010
+ const lines = [
65011
+ {
65012
+ text: `Held: ${which} waits for an owner or admin before it goes on production.`,
65013
+ tone: "warn"
65014
+ },
65015
+ { text: holdWhy(deploy, slug) }
65016
+ ];
65017
+ if (deploy.heldUrl) {
65018
+ lines.push({ text: `The team can call it, signed in, at ${deploy.heldUrl}` });
65019
+ if (deploy.number !== undefined) {
65020
+ lines.push({
65021
+ text: `sokie call <tool> --held ${deploy.number} tries a tool there.`,
65022
+ tone: "dim"
65023
+ });
65024
+ }
65025
+ }
65026
+ const lapses = deploy.hold ? ` If no one does by ${onDay(deploy.hold.until)}, it lapses.` : "";
65027
+ lines.push({
65028
+ text: `Production stays as it is until an owner or admin puts it live; sokie requests lists it for them.${lapses}`,
65029
+ tone: "dim"
65030
+ });
65031
+ return lines;
65032
+ }
65033
+
64850
65034
  // src/init/gitignore.ts
64851
65035
  import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
64852
65036
  import { join as join8 } from "node:path";
@@ -64872,6 +65056,44 @@ function liveLines(server, deployment) {
64872
65056
  const live = `Live: ${server.url ?? server.slug}${number4}`;
64873
65057
  return server.liveDeploymentId ? [live, SWITCH_OVER_NOTE] : [live];
64874
65058
  }
65059
+ async function reportDeploy(out, server, deployment) {
65060
+ const { status } = deployment;
65061
+ if (status === "failed") {
65062
+ for (const finding3 of deployment.findings ?? []) {
65063
+ await out.problem(findingLine(finding3), LEVEL_TONE[finding3.level]);
65064
+ }
65065
+ const reason = deployment.error?.message ?? "The deploy failed.";
65066
+ await out.result({ ok: false, status, server, deployment, error: deployment.error ?? null });
65067
+ await out.problem(`✗ ${reason}`, "fail");
65068
+ return EXIT.no;
65069
+ }
65070
+ const url2 = server.url;
65071
+ if (status === "held") {
65072
+ for (const line of heldLines(server.slug, deployment))
65073
+ await out.line(line.text, line.tone);
65074
+ const { heldUrl = null, hold = null } = deployment;
65075
+ await out.result({
65076
+ ok: true,
65077
+ status,
65078
+ server,
65079
+ deployment,
65080
+ url: url2,
65081
+ heldUrl,
65082
+ hold,
65083
+ linkFile: LINK_FILE
65084
+ });
65085
+ return deployment;
65086
+ }
65087
+ if (status === "live") {
65088
+ const [live, ...rest] = liveLines(server, deployment);
65089
+ if (live)
65090
+ await out.line(live, "ok");
65091
+ for (const line of rest)
65092
+ await out.line(line, "dim");
65093
+ }
65094
+ await out.result({ ok: true, status, server, deployment, url: url2, linkFile: LINK_FILE });
65095
+ return deployment;
65096
+ }
64875
65097
  function platformFor(client) {
64876
65098
  return {
64877
65099
  query: client.query.bind(client),
@@ -64916,25 +65138,7 @@ async function run4(argv, out) {
64916
65138
  progress: (status) => out.line(` ${status}`, "dim"),
64917
65139
  ...base === undefined || values.force ? {} : { base }
64918
65140
  });
64919
- const url2 = server.url;
64920
- if (deployment.status === "failed") {
64921
- for (const finding3 of deployment.findings ?? []) {
64922
- await out.problem(findingLine(finding3), LEVEL_TONE[finding3.level]);
64923
- }
64924
- const reason = deployment.error?.message ?? "The deploy failed.";
64925
- await out.result({ ok: false, server, deployment, error: deployment.error ?? null });
64926
- await out.problem(`✗ ${reason}`, "fail");
64927
- return EXIT.no;
64928
- }
64929
- if (deployment.status === "live") {
64930
- const [live, ...rest] = liveLines(server, deployment);
64931
- if (live)
64932
- await out.line(live, "ok");
64933
- for (const line of rest)
64934
- await out.line(line, "dim");
64935
- }
64936
- await out.result({ ok: true, server, deployment, url: url2, linkFile: LINK_FILE });
64937
- return deployment;
65141
+ return await reportDeploy(out, server, deployment);
64938
65142
  };
64939
65143
  const exit = (shipped) => typeof shipped === "number" ? shipped : EXIT.ok;
64940
65144
  const deployKey = deployKeyFrom();
@@ -64987,7 +65191,7 @@ async function run4(argv, out) {
64987
65191
  });
64988
65192
  ensureIgnored(root, ".sokie/");
64989
65193
  const shipped = await ship(platformFor(client), server, base);
64990
- if (!github && typeof shipped !== "number" && shipped.number !== undefined) {
65194
+ if (!github && typeof shipped !== "number" && shipped.number !== undefined && shipped.status !== "held") {
64991
65195
  writeLink(root, info2.site, {
64992
65196
  serverId: server.serverId,
64993
65197
  slug: server.slug,
@@ -65026,6 +65230,15 @@ sokie edit, a deploy is refused once production has moved on since the
65026
65230
  files were downloaded (an edit in the app, a restore), so it never undoes
65027
65231
  a change unseen: sokie pull brings that change in first.
65028
65232
 
65233
+ A server can make production wait for an owner or admin. A member's deploy
65234
+ that must wait (any, with the server's switch on; otherwise one that adds a
65235
+ delete or outside-the-API tool) builds, runs its cases and is held: it
65236
+ serves only the team, signed in, at its own address, for up to 7 days.
65237
+ Deploys from GitHub or CI wait only when the server says so. The command
65238
+ stops there, says why, where and until when, and exits 0; --json gives
65239
+ "status": "held" with "heldUrl" and "hold". sokie call --held <n> tries it,
65240
+ and an owner or admin puts it live from sokie requests.
65241
+
65029
65242
  --no-wait Return once the deploy is queued.
65030
65243
  --force Deploy these files even if production changed since they were downloaded.
65031
65244
  ${SITE_HELP}
@@ -65121,109 +65334,11 @@ function diffLines(view) {
65121
65334
  return lines;
65122
65335
  }
65123
65336
 
65124
- // src/deploy-list.ts
65125
- function statusOf(deploy) {
65126
- if (deploy.status === "failed")
65127
- return { text: "failed", tone: "fail" };
65128
- if (deploy.status === "queued" || deploy.status === "building") {
65129
- return { text: deploy.status, tone: "warn" };
65130
- }
65131
- if (deploy.previewPr !== undefined)
65132
- return { text: "preview", tone: deploy.status === "live" ? "ok" : undefined };
65133
- return deploy.status === "live" ? { text: "live", tone: "ok" } : { text: "replaced", tone: "dim" };
65134
- }
65135
- function sourceOf(deploy) {
65136
- if (deploy.restoreOf !== undefined)
65137
- return `restore of #${deploy.restoreOf}`;
65138
- const source = deploy.source;
65139
- if (!source)
65140
- return "uploaded from the CLI";
65141
- if (source.kind === "upload") {
65142
- const edited = source.edited ?? [];
65143
- if (edited.length > 0) {
65144
- const named = edited.length <= 2 ? edited.join(" and ") : `${edited[0]} and ${edited.length - 1} more`;
65145
- return `${named} edited in the app`;
65146
- }
65147
- return source.from === "web" ? "made in Sokie from a spec" : "uploaded from the CLI";
65148
- }
65149
- const pr = deploy.previewPr ?? source.prNumber;
65150
- const sha = source.sha.slice(0, 7);
65151
- return pr === undefined ? `${sha} on ${source.branch}` : `${sha} · preview #${pr}`;
65152
- }
65153
- var MINUTE = 60000;
65154
- var HOUR = 60 * MINUTE;
65155
- var DAY = 24 * HOUR;
65156
- function ago(at2, now) {
65157
- const gone = Math.max(0, now - at2);
65158
- if (gone < MINUTE)
65159
- return "just now";
65160
- if (gone < HOUR)
65161
- return `${Math.floor(gone / MINUTE)} min ago`;
65162
- if (gone < DAY)
65163
- return `${Math.floor(gone / HOUR)} h ago`;
65164
- if (gone < 2 * DAY)
65165
- return "yesterday";
65166
- if (gone < 30 * DAY)
65167
- return `${Math.floor(gone / DAY)} days ago`;
65168
- return new Date(at2).toLocaleDateString("en-US", {
65169
- month: "short",
65170
- day: "numeric",
65171
- timeZone: "UTC"
65172
- });
65173
- }
65174
- function headerLine(slug, facts) {
65175
- if (facts.pin) {
65176
- const since = facts.pin.skippedPushes === 1 ? "1 push" : `${facts.pin.skippedPushes} pushes`;
65177
- return {
65178
- text: `${slug} · production stays on #${facts.live ?? "?"} until sokie deploys resume (${since} since the restore)`,
65179
- tone: "warn"
65180
- };
65181
- }
65182
- if (facts.branch)
65183
- return { text: `${slug} · production follows ${facts.branch}`, tone: "dim" };
65184
- return { text: `${slug} · Sokie keeps its files`, tone: "dim" };
65185
- }
65186
- function rowLine(deploy, now) {
65187
- const status = statusOf(deploy);
65188
- const number4 = deploy.number === undefined ? "—" : `#${deploy.number}`;
65189
- const text5 = ` ${number4.padEnd(5)} ${status.text.padEnd(9)} ${sourceOf(deploy).padEnd(32)} ${ago(deploy.createdAt, now)}`;
65190
- return { text: text5, tone: status.tone === "dim" ? undefined : status.tone };
65191
- }
65192
- function listLines2(slug, deploys, facts) {
65193
- const live = deploys.find((d) => d.status === "live" && d.previewPr === undefined)?.number;
65194
- const lines = [headerLine(slug, { ...facts, live }), { text: "" }];
65195
- if (deploys.length === 0) {
65196
- lines.push({ text: " No deploys yet. sokie deploy makes the first one.", tone: "dim" });
65197
- return lines;
65198
- }
65199
- for (const deploy of deploys)
65200
- lines.push(rowLine(deploy, facts.now));
65201
- if (facts.more) {
65202
- lines.push({
65203
- text: ` … older ones too. sokie deploys --limit ${facts.limit * 2} goes further back; --json for scripts.`,
65204
- tone: "dim"
65205
- });
65206
- }
65207
- return lines;
65208
- }
65209
- function diffTone(line, index) {
65210
- if (index === 0)
65211
- return "strong";
65212
- const sign = line.trimStart().slice(0, 2);
65213
- if (sign === "+ ")
65214
- return "ok";
65215
- if (sign === "- ")
65216
- return "fail";
65217
- if (sign === "↑ " || sign === "↓ ")
65218
- return "warn";
65219
- return;
65220
- }
65221
-
65222
65337
  // src/deploys-restore.ts
65223
65338
  var REFUSED = {
65224
65339
  ALREADY_LIVE: (n) => `#${n} is what production runs now. sokie deploys lists the others.`,
65225
65340
  DEPLOY_NOT_FOUND: (n) => `There is no deploy #${n} on this server.`,
65226
- NOT_RESTORABLE: (n) => `#${n} never served production: it's a preview, it failed, or it hasn't finished. Only a deploy that went live and was replaced can be restored.`,
65341
+ NOT_RESTORABLE: (n) => `#${n} never served production: it's a preview, it failed, it was held and never put live, or it hasn't finished. Only a deploy that went live and was replaced can be restored.`,
65227
65342
  DEPLOY_IN_PROGRESS: () => "A deploy is running. Try again when it finishes.",
65228
65343
  MISSING_SECRETS: (n) => `#${n} needs a secret production no longer has.`,
65229
65344
  PLAN_LIMIT_SERVERS: () => "This server is past your plan's server limit, so it takes no deploys. Delete a server or upgrade on the Billing page.",
@@ -65253,8 +65368,8 @@ async function restoreDeploy(out, client, server, n, options, deps) {
65253
65368
  if (check3.refusal)
65254
65369
  return await refused2(out, new ConvexError(check3.refusal), n) ?? EXIT.no;
65255
65370
  if (check3.missingSecrets.length > 0) {
65256
- const names = check3.missingSecrets.join(", ");
65257
- return fail(out, "missing_secrets", `#${n} reads ${names}, which production doesn't have any more. Set it with sokie secrets set ${check3.missingSecrets[0]}, or restore a later deploy.`, EXIT.no);
65371
+ const names2 = check3.missingSecrets.join(", ");
65372
+ return fail(out, "missing_secrets", `#${n} reads ${names2}, which production doesn't have any more. Set it with sokie secrets set ${check3.missingSecrets[0]}, or restore a later deploy.`, EXIT.no);
65258
65373
  }
65259
65374
  const page = await client.query(fn.server, { serverId }).catch(() => null);
65260
65375
  const live = page?.live?.number;
@@ -65293,6 +65408,18 @@ async function restoreDeploy(out, client, server, n, options, deps) {
65293
65408
  });
65294
65409
  return EXIT.no;
65295
65410
  }
65411
+ if (done.status === "held") {
65412
+ for (const line of heldLines(server.slug, done))
65413
+ await out.line(line.text, line.tone);
65414
+ const { status, heldUrl = null, hold = null } = done;
65415
+ await out.result({
65416
+ ok: true,
65417
+ server: server.slug,
65418
+ deploy: { number: queued.number, restoreOf: n, status, heldUrl, hold },
65419
+ pinned: queued.pinned
65420
+ });
65421
+ return EXIT.ok;
65422
+ }
65296
65423
  const took = Math.max(1, Math.round((Date.now() - started) / 1000));
65297
65424
  await out.line(`#${queued.number} is live in ${took} s · ${server.url ?? server.slug}`, "ok");
65298
65425
  await out.line("Calls can reach the previous deploy for up to a minute while the edges catch up.", "dim");
@@ -65467,7 +65594,9 @@ var deploysCommand = {
65467
65594
  summary: "List a server's deploys, compare two, or restore an earlier one.",
65468
65595
  help: `With no verb, lists the newest deploys (--limit, default 10): each one's
65469
65596
  number, status, where its files came from and when, under what production
65470
- follows now.
65597
+ follows now. A held deploy, waiting for an owner or admin, shows until when
65598
+ and the address the team can call it at; one that never went live says
65599
+ whether it was declined, withdrawn or lapsed.
65471
65600
 
65472
65601
  diff [<from>] <to>
65473
65602
  Compares two deploys' bundles and prints what an agent sees differently:
@@ -66336,19 +66465,19 @@ async function followWorkspace(out, client, deps) {
66336
66465
  return null;
66337
66466
  }
66338
66467
  async function followNames(out, client, domain2, deps, left) {
66339
- let names = [];
66468
+ let names2 = [];
66340
66469
  for (let waited = 0;waited < left; waited += POLL_MS2) {
66341
- names = (await allHostnames(client)).filter((h) => h.hostname.endsWith(`.${domain2}`));
66342
- if (names.length > 0 && names.every((h) => h.status === "live")) {
66343
- const list = names.map((h) => h.serverName).join(", ");
66344
- await out.line(` ● ${names.length} server${names.length === 1 ? "" : "s"} live under it: ${list}`, "ok");
66470
+ names2 = (await allHostnames(client)).filter((h) => h.hostname.endsWith(`.${domain2}`));
66471
+ if (names2.length > 0 && names2.every((h) => h.status === "live")) {
66472
+ const list = names2.map((h) => h.serverName).join(", ");
66473
+ await out.line(` ● ${names2.length} server${names2.length === 1 ? "" : "s"} live under it: ${list}`, "ok");
66345
66474
  await out.line("New servers get theirs as they are made.", "dim");
66346
- return names;
66475
+ return names2;
66347
66476
  }
66348
66477
  await deps.sleep(POLL_MS2);
66349
66478
  }
66350
66479
  await out.line("Servers are still getting their certificates; sokie domains shows each.", "dim");
66351
- return names;
66480
+ return names2;
66352
66481
  }
66353
66482
  async function followExact(out, client, server, deps) {
66354
66483
  let said = "";
@@ -66398,8 +66527,8 @@ async function addDomain(out, client, domain2, options, deps) {
66398
66527
  return EXIT.ok;
66399
66528
  }
66400
66529
  await out.line("Waiting for both (Ctrl+C stops waiting; Sokie keeps checking)…", "dim");
66401
- const names = await followWorkspace(out, client, deps);
66402
- await out.result({ ok: true, domain: added2, dns: dns2, ...names ? { hostnames: names } : {} });
66530
+ const names2 = await followWorkspace(out, client, deps);
66531
+ await out.result({ ok: true, domain: added2, dns: dns2, ...names2 ? { hostnames: names2 } : {} });
66403
66532
  return EXIT.ok;
66404
66533
  }
66405
66534
  const added = await client.mutation(domainFn.addExact, { serverId: server.serverId, domain: domain2 });
@@ -67859,9 +67988,8 @@ var requestFn = {
67859
67988
  };
67860
67989
 
67861
67990
  // src/requests.ts
67862
- var day = new Intl.DateTimeFormat("en", { month: "short", day: "numeric", timeZone: "UTC" });
67863
67991
  function lapsesOn(request) {
67864
- return day.format(request.expiresAt);
67992
+ return onDay(request.expiresAt);
67865
67993
  }
67866
67994
  async function allRequests(client, list) {
67867
67995
  const rows = [];
@@ -67938,8 +68066,9 @@ function rowLines(request, short, headline, now) {
67938
68066
  const lines = [{ text: ` ${id} ${headline}` }];
67939
68067
  if (request.reason)
67940
68068
  lines.push({ text: `${indent2}${quoted(request.reason)}` });
68069
+ const since = request.change.kind === "deploy.promote" ? "held" : "asked";
67941
68070
  lines.push({
67942
- text: `${indent2}asked ${ago(request.createdAt, now)} · lapses ${lapsesOn(request)}`,
68071
+ text: `${indent2}${since} ${ago(request.createdAt, now)} · lapses ${lapsesOn(request)}`,
67943
68072
  tone: "dim"
67944
68073
  });
67945
68074
  return lines;
@@ -67976,12 +68105,55 @@ function listLines3(waiting, mine, now) {
67976
68105
  lines.push({ text: "" }, { text: `sokie requests ${hints.join("; ")}.`, tone: "dim" });
67977
68106
  return lines;
67978
68107
  }
68108
+ var AUTOMATED = new Set(["GitHub", "A CI deploy key"]);
68109
+ function emailed(request) {
68110
+ return !(request.change.kind === "deploy.promote" && AUTOMATED.has(request.requesterName));
68111
+ }
68112
+ function answeredLines(request, outcome, noted) {
68113
+ const { requesterName: who, words: words2, change } = request;
68114
+ const person = emailed(request);
68115
+ const hears = person ? ` ${who} hears by email.` : "";
68116
+ const theyHear = person ? `They hear by email${noted ? ", with your note" : ""}.` : "";
68117
+ const promote = change.kind === "deploy.promote";
68118
+ if (outcome === "approved") {
68119
+ return [
68120
+ { text: `Approved: ${words2}.`, tone: "ok" },
68121
+ {
68122
+ text: promote ? `It builds again with the secrets set now, then goes live; sokie deploys shows it.${hears}` : `Sokie made the change as you, and ${who} hears by email.`,
68123
+ tone: "dim"
68124
+ }
68125
+ ];
68126
+ }
68127
+ if (outcome === "declined") {
68128
+ return [
68129
+ { text: `Declined ${who}'s request to ${words2}.`, tone: "ok" },
68130
+ {
68131
+ text: promote ? `Its held address stops answering. ${theyHear}`.trim() : theyHear,
68132
+ tone: "dim"
68133
+ }
68134
+ ];
68135
+ }
68136
+ return [
68137
+ {
68138
+ text: `Closed it: the request to ${words2} no longer applied, so nothing changed.`,
68139
+ tone: "warn"
68140
+ },
68141
+ {
68142
+ text: promote ? `Deploy #${change.number} isn't held any more; sokie deploys says what became of it.${hears}` : `The server is gone, or the change was made another way.${hears}`,
68143
+ tone: "dim"
68144
+ }
68145
+ ];
68146
+ }
67979
68147
  function refused4(out, error62, gone) {
67980
68148
  if (!(error62 instanceof ConvexError))
67981
68149
  return null;
67982
68150
  const data = error62.data;
67983
68151
  if (data === "NOT_FOUND")
67984
68152
  return fail(out, gone.code, gone.message, EXIT.no);
68153
+ if (data === "DEPLOY_IN_PROGRESS") {
68154
+ const message2 = "Another production deploy of this server is still running. Approve again once it finishes; sokie deploys shows it.";
68155
+ return fail(out, "deploy_in_progress", message2, EXIT.no);
68156
+ }
67985
68157
  if (typeof data !== "object" || data === null)
67986
68158
  return null;
67987
68159
  const { code: code2, message } = data;
@@ -68069,15 +68241,8 @@ async function answer(out, client, given, how, deps) {
68069
68241
  approve: how.approve,
68070
68242
  ...how.note ? { note: how.note } : {}
68071
68243
  });
68072
- if (outcome === "approved") {
68073
- await out.line(`Approved: ${request.words}.`, "ok");
68074
- await out.line(`Sokie made the change as you, and ${request.requesterName} hears by email.`, "dim");
68075
- } else if (outcome === "declined") {
68076
- await out.line(`Declined ${request.requesterName}'s request to ${request.words}.`, "ok");
68077
- await out.line(`They hear by email${how.note ? ", with your note" : ""}.`, "dim");
68078
- } else {
68079
- await out.line(`Closed it: the request to ${request.words} no longer applied, so nothing changed.`, "warn");
68080
- await out.line(`The server is gone, or the change was made another way. ${request.requesterName} hears by email.`, "dim");
68244
+ for (const line of answeredLines(request, outcome, how.note !== undefined)) {
68245
+ await out.line(line.text, line.tone);
68081
68246
  }
68082
68247
  await out.result({ ok: true, requestId: request.requestId, outcome, request });
68083
68248
  return EXIT.ok;
@@ -68088,6 +68253,9 @@ async function withdraw(out, client, given) {
68088
68253
  return request;
68089
68254
  await client.mutation(requestFn.withdraw, { requestId: request.requestId });
68090
68255
  await out.line(`Withdrew your request to ${request.words}.`, "ok");
68256
+ if (request.change.kind === "deploy.promote") {
68257
+ await out.line("That ends the held deploy: its held address stops answering.", "dim");
68258
+ }
68091
68259
  await out.result({ ok: true, requestId: request.requestId, outcome: "withdrawn", request });
68092
68260
  return EXIT.ok;
68093
68261
  }
@@ -68153,6 +68321,11 @@ it's live, and moving a live name under the workspace domain are for owners
68153
68321
  and admins. A member asks instead; any one owner or admin approves, which
68154
68322
  makes the change as them, or declines. A request waits 7 days, then lapses.
68155
68323
 
68324
+ A held deploy asks too: when a server makes production wait for an owner or
68325
+ admin, a deploy that must wait (sokie deploy says so) is listed here as a
68326
+ request to put it on production. Approving builds it again with the secrets
68327
+ set now and puts it live; declining or withdrawing ends it.
68328
+
68156
68329
  With no verb, lists the requests waiting for your answer (as an owner or
68157
68330
  admin) and the ones you asked, in the workspace commands work in: each one's
68158
68331
  id, who asked for what and why, and when it lapses.
@@ -68166,7 +68339,7 @@ decline <id>
68166
68339
  --note <text> A word back to them, up to 500 characters.
68167
68340
 
68168
68341
  withdraw <id>
68169
- Takes back a request you asked.
68342
+ Takes back a request you asked; for your held deploy, that ends it.
68170
68343
 
68171
68344
  ask <kind> <server> [<new-name>]
68172
68345
  Asks the workspace's owners and admins, who hear by email. <server> is the
@@ -68227,8 +68400,8 @@ async function run10(argv, out) {
68227
68400
  cursor = page2.continueCursor;
68228
68401
  }
68229
68402
  for (const secret of secrets) {
68230
- const day2 = new Date(secret.updatedAt).toISOString().slice(0, 10);
68231
- await out.line(`${secret.name} ${out.tint("dim", `set ${day2}`)}`);
68403
+ const day = new Date(secret.updatedAt).toISOString().slice(0, 10);
68404
+ await out.line(`${secret.name} ${out.tint("dim", `set ${day}`)}`);
68232
68405
  }
68233
68406
  if (secrets.length === 0)
68234
68407
  await out.line(`${server.slug} has no secrets set.`);
@@ -68317,7 +68490,7 @@ function caseJson(run11) {
68317
68490
  var TERMINAL4 = { call: callTool, signedIn: withSignedIn, now: Date.now };
68318
68491
  var RENEW_MS = 30000;
68319
68492
  function caseTarget(target) {
68320
- return target.kind;
68493
+ return target.kind === "held" ? "production" : target.kind;
68321
68494
  }
68322
68495
  async function runCases(out, deps, plan, target, endpointFor, all) {
68323
68496
  const first = await endpointFor();
@@ -68381,7 +68554,7 @@ async function runTest(argv, out, deps) {
68381
68554
  if (positionals.length > 0) {
68382
68555
  throw new UsageError("sokie test runs every case; it takes no names. --dir picks the repository.");
68383
68556
  }
68384
- const target = targetFrom(values.preview, values.production);
68557
+ const target = targetFrom(values);
68385
68558
  if (target.kind === "dev" && values.server !== undefined) {
68386
68559
  throw new UsageError("--server picks a deploy: add --preview <n> or --production.");
68387
68560
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abyssinia-labs/sokie",
3
3
  "type": "module",
4
- "version": "0.7.0-staging.138",
4
+ "version": "0.8.0",
5
5
  "description": "Turn an OpenAPI or Swagger spec into an MCP server, run it on your machine, and deploy it to Sokie. The sokie command (alias sok).",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://sokie.sh/docs/cli",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "author": "Abyssinia Labs <hello@sokie.sh> (https://sokie.sh)",
12
12
  "sokie": {
13
- "site": "https://staging.sokie.sh"
13
+ "site": "https://sokie.sh"
14
14
  },
15
15
  "keywords": [
16
16
  "mcp",