@shipstatic/types 2.25.0-beta.1 → 2.26.0-beta.1

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/dist/index.d.ts CHANGED
@@ -135,7 +135,7 @@ export interface Deployment {
135
135
  readonly created: number;
136
136
  /** Unix timestamp (seconds) when deployment expires, null if never */
137
137
  expires: number | null;
138
- /** Full URL to the deployment screenshot (e.g., 'https://screenshots.shipstatic.com/happy-cat-abc1234/a3f2c1b4d5e6f789') */
138
+ /** Full URL to the deployment screenshot, rendered on the first request for it (e.g., 'https://screenshots.shipstatic.com/happy-cat-abc1234/a3f2c1b4d5e6f789') */
139
139
  readonly screenshot: string;
140
140
  }
141
141
  /**
package/dist/schemas.js CHANGED
@@ -75,7 +75,7 @@ export const DeploymentSchema = z.object({
75
75
  .describe(`Unix timestamp (seconds) when the deployment expires; null when permanent. Anonymous deployments expire ${PUBLIC_DEPLOYMENT_TTL_SECONDS / 86_400} days after creation unless claimed; an authenticated deployment carries one only when it requested a ttl.`),
76
76
  screenshot: z
77
77
  .url()
78
- .describe('Full URL to the deployment screenshot. Captured asynchronously after deploy; the URL is returned immediately but the image may take a few seconds to become available.'),
78
+ .describe('Full URL to the deployment screenshot. Rendered on the first request for it, so the URL is returned immediately and the first request takes a few seconds; every request after that is served immediately.'),
79
79
  });
80
80
  export const DeploymentCreateResponseSchema = DeploymentSchema.extend({
81
81
  claim: z
package/dist/time.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * How long a deployment has left, in the words a person reads.
3
+ *
4
+ * When this module was written (2026-09-17) one deadline was told four ways:
5
+ * the deploy card counted from now, the CLI and the marketing site from the
6
+ * deployment's creation, the VS Code palette quoted the anonymous tier's fixed
7
+ * lifetime, and the dashboard truncated, so a deployment made a second ago
8
+ * read "2d" there and "3 days" everywhere else. The rule since: a surface that
9
+ * tells a person how long a deployment has left says it with
10
+ * `formatTimeRemaining`, and a duration a person reads about deployments is
11
+ * spelled by `formatDuration`.
12
+ *
13
+ * The WORDS are the surface's own. A card says "Expires in", the marketing
14
+ * site says "It stays live for", and each says something different once the
15
+ * deadline has passed, which is why that case is `null` rather than a
16
+ * sentence. What is owned here is the part that drifted silently: the unit,
17
+ * the rounding, and what the clock is measured from.
18
+ *
19
+ * A subpath export (`@shipstatic/types/time`), for the same reason as
20
+ * `/schemas`: a browser bundle importing only this carries only this. From the
21
+ * main entry it would carry about a kilobyte of unrelated constants that a
22
+ * bundler cannot prove are unused (1 KB gzipped, measured with the deploy
23
+ * card's esbuild on 2026-09-17).
24
+ */
25
+ /**
26
+ * A span of time in words: `"3 days"`, `"5 hours"`, `"45 minutes"`.
27
+ *
28
+ * Minutes under two hours, hours under two days, and days beyond, each rounded
29
+ * to the nearest. The unit changes exactly where the larger one rounds to two,
30
+ * so the number never jumps as time passes (119 minutes, then 2 hours), and
31
+ * the two vaguest things a rounded clock can say, "1 hour" and "1 day", are
32
+ * never said. Rounding rather than truncating is what lets a deployment made a
33
+ * second ago read the lifetime it was given: "3 days", not "2 days". Days are
34
+ * the largest unit because nobody counts a deadline in weeks, and a month has
35
+ * no fixed length.
36
+ *
37
+ * A span that rounds to no minutes at all, zero and negative included, reads
38
+ * "less than a minute".
39
+ */
40
+ export declare function formatDuration(seconds: number): string;
41
+ /**
42
+ * The time left before a deadline, in words (`"3 days"`), or `null` once it
43
+ * has passed.
44
+ *
45
+ * `expires` is unix SECONDS, the wire's own field, taken as it arrives so that
46
+ * no caller converts it: the deploy card once read it as a date string and
47
+ * showed the same three days on every deployment for months. `now` is seconds
48
+ * too, and defaults to the clock.
49
+ */
50
+ export declare function formatTimeRemaining(expires: number, now?: number): string | null;
package/dist/time.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * How long a deployment has left, in the words a person reads.
3
+ *
4
+ * When this module was written (2026-09-17) one deadline was told four ways:
5
+ * the deploy card counted from now, the CLI and the marketing site from the
6
+ * deployment's creation, the VS Code palette quoted the anonymous tier's fixed
7
+ * lifetime, and the dashboard truncated, so a deployment made a second ago
8
+ * read "2d" there and "3 days" everywhere else. The rule since: a surface that
9
+ * tells a person how long a deployment has left says it with
10
+ * `formatTimeRemaining`, and a duration a person reads about deployments is
11
+ * spelled by `formatDuration`.
12
+ *
13
+ * The WORDS are the surface's own. A card says "Expires in", the marketing
14
+ * site says "It stays live for", and each says something different once the
15
+ * deadline has passed, which is why that case is `null` rather than a
16
+ * sentence. What is owned here is the part that drifted silently: the unit,
17
+ * the rounding, and what the clock is measured from.
18
+ *
19
+ * A subpath export (`@shipstatic/types/time`), for the same reason as
20
+ * `/schemas`: a browser bundle importing only this carries only this. From the
21
+ * main entry it would carry about a kilobyte of unrelated constants that a
22
+ * bundler cannot prove are unused (1 KB gzipped, measured with the deploy
23
+ * card's esbuild on 2026-09-17).
24
+ */
25
+ /**
26
+ * A span of time in words: `"3 days"`, `"5 hours"`, `"45 minutes"`.
27
+ *
28
+ * Minutes under two hours, hours under two days, and days beyond, each rounded
29
+ * to the nearest. The unit changes exactly where the larger one rounds to two,
30
+ * so the number never jumps as time passes (119 minutes, then 2 hours), and
31
+ * the two vaguest things a rounded clock can say, "1 hour" and "1 day", are
32
+ * never said. Rounding rather than truncating is what lets a deployment made a
33
+ * second ago read the lifetime it was given: "3 days", not "2 days". Days are
34
+ * the largest unit because nobody counts a deadline in weeks, and a month has
35
+ * no fixed length.
36
+ *
37
+ * A span that rounds to no minutes at all, zero and negative included, reads
38
+ * "less than a minute".
39
+ */
40
+ export function formatDuration(seconds) {
41
+ const minutes = Math.round(seconds / 60);
42
+ if (!(minutes >= 1))
43
+ return 'less than a minute';
44
+ if (minutes < 120)
45
+ return countOf(minutes, 'minute');
46
+ const hours = Math.round(seconds / 3_600);
47
+ if (hours < 48)
48
+ return countOf(hours, 'hour');
49
+ return countOf(Math.round(seconds / 86_400), 'day');
50
+ }
51
+ /**
52
+ * The time left before a deadline, in words (`"3 days"`), or `null` once it
53
+ * has passed.
54
+ *
55
+ * `expires` is unix SECONDS, the wire's own field, taken as it arrives so that
56
+ * no caller converts it: the deploy card once read it as a date string and
57
+ * showed the same three days on every deployment for months. `now` is seconds
58
+ * too, and defaults to the clock.
59
+ */
60
+ export function formatTimeRemaining(expires, now = Date.now() / 1_000) {
61
+ const seconds = expires - now;
62
+ return seconds > 0 ? formatDuration(seconds) : null;
63
+ }
64
+ function countOf(count, unit) {
65
+ return `${count} ${unit}${count === 1 ? '' : 's'}`;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.25.0-beta.1",
3
+ "version": "2.26.0-beta.1",
4
4
  "description": "Shared TypeScript types for the ShipStatic platform.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,12 +13,19 @@
13
13
  "./schemas": {
14
14
  "types": "./dist/schemas.d.ts",
15
15
  "default": "./dist/schemas.js"
16
+ },
17
+ "./time": {
18
+ "types": "./dist/time.d.ts",
19
+ "default": "./dist/time.js"
16
20
  }
17
21
  },
18
22
  "typesVersions": {
19
23
  "*": {
20
24
  "schemas": [
21
25
  "./dist/schemas.d.ts"
26
+ ],
27
+ "time": [
28
+ "./dist/time.d.ts"
22
29
  ]
23
30
  }
24
31
  },
package/src/index.ts CHANGED
@@ -144,7 +144,7 @@ export interface Deployment {
144
144
  readonly created: number;
145
145
  /** Unix timestamp (seconds) when deployment expires, null if never */
146
146
  expires: number | null; // Mutable - can be updated
147
- /** Full URL to the deployment screenshot (e.g., 'https://screenshots.shipstatic.com/happy-cat-abc1234/a3f2c1b4d5e6f789') */
147
+ /** Full URL to the deployment screenshot, rendered on the first request for it (e.g., 'https://screenshots.shipstatic.com/happy-cat-abc1234/a3f2c1b4d5e6f789') */
148
148
  readonly screenshot: string;
149
149
  }
150
150
 
package/src/schemas.ts CHANGED
@@ -94,7 +94,7 @@ export const DeploymentSchema = z.object({
94
94
  screenshot: z
95
95
  .url()
96
96
  .describe(
97
- 'Full URL to the deployment screenshot. Captured asynchronously after deploy; the URL is returned immediately but the image may take a few seconds to become available.',
97
+ 'Full URL to the deployment screenshot. Rendered on the first request for it, so the URL is returned immediately and the first request takes a few seconds; every request after that is served immediately.',
98
98
  ),
99
99
  });
100
100
 
package/src/time.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * How long a deployment has left, in the words a person reads.
3
+ *
4
+ * When this module was written (2026-09-17) one deadline was told four ways:
5
+ * the deploy card counted from now, the CLI and the marketing site from the
6
+ * deployment's creation, the VS Code palette quoted the anonymous tier's fixed
7
+ * lifetime, and the dashboard truncated, so a deployment made a second ago
8
+ * read "2d" there and "3 days" everywhere else. The rule since: a surface that
9
+ * tells a person how long a deployment has left says it with
10
+ * `formatTimeRemaining`, and a duration a person reads about deployments is
11
+ * spelled by `formatDuration`.
12
+ *
13
+ * The WORDS are the surface's own. A card says "Expires in", the marketing
14
+ * site says "It stays live for", and each says something different once the
15
+ * deadline has passed, which is why that case is `null` rather than a
16
+ * sentence. What is owned here is the part that drifted silently: the unit,
17
+ * the rounding, and what the clock is measured from.
18
+ *
19
+ * A subpath export (`@shipstatic/types/time`), for the same reason as
20
+ * `/schemas`: a browser bundle importing only this carries only this. From the
21
+ * main entry it would carry about a kilobyte of unrelated constants that a
22
+ * bundler cannot prove are unused (1 KB gzipped, measured with the deploy
23
+ * card's esbuild on 2026-09-17).
24
+ */
25
+
26
+ /**
27
+ * A span of time in words: `"3 days"`, `"5 hours"`, `"45 minutes"`.
28
+ *
29
+ * Minutes under two hours, hours under two days, and days beyond, each rounded
30
+ * to the nearest. The unit changes exactly where the larger one rounds to two,
31
+ * so the number never jumps as time passes (119 minutes, then 2 hours), and
32
+ * the two vaguest things a rounded clock can say, "1 hour" and "1 day", are
33
+ * never said. Rounding rather than truncating is what lets a deployment made a
34
+ * second ago read the lifetime it was given: "3 days", not "2 days". Days are
35
+ * the largest unit because nobody counts a deadline in weeks, and a month has
36
+ * no fixed length.
37
+ *
38
+ * A span that rounds to no minutes at all, zero and negative included, reads
39
+ * "less than a minute".
40
+ */
41
+ export function formatDuration(seconds: number): string {
42
+ const minutes = Math.round(seconds / 60);
43
+ if (!(minutes >= 1)) return 'less than a minute';
44
+ if (minutes < 120) return countOf(minutes, 'minute');
45
+ const hours = Math.round(seconds / 3_600);
46
+ if (hours < 48) return countOf(hours, 'hour');
47
+ return countOf(Math.round(seconds / 86_400), 'day');
48
+ }
49
+
50
+ /**
51
+ * The time left before a deadline, in words (`"3 days"`), or `null` once it
52
+ * has passed.
53
+ *
54
+ * `expires` is unix SECONDS, the wire's own field, taken as it arrives so that
55
+ * no caller converts it: the deploy card once read it as a date string and
56
+ * showed the same three days on every deployment for months. `now` is seconds
57
+ * too, and defaults to the clock.
58
+ */
59
+ export function formatTimeRemaining(
60
+ expires: number,
61
+ now: number = Date.now() / 1_000,
62
+ ): string | null {
63
+ const seconds = expires - now;
64
+ return seconds > 0 ? formatDuration(seconds) : null;
65
+ }
66
+
67
+ function countOf(count: number, unit: string): string {
68
+ return `${count} ${unit}${count === 1 ? '' : 's'}`;
69
+ }