@shardflux/cli 0.3.0 → 0.4.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.
@@ -0,0 +1,7 @@
1
+ /** A recipe as template.yaml, with `header` as the comment above the document. */
2
+ export declare function recipeYaml(recipe: unknown, header?: string): string;
3
+ /**
4
+ * The starter template.yaml on `base` (`<slug>@<version>`): every section present, with commented examples and one
5
+ * placeholder step (the API refuses a recipe that builds nothing). It parses to a valid recipe v2 and builds as it is.
6
+ */
7
+ export declare function initTemplateYaml(base: string): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * template.yaml text (contracts §24.1: the recipe v2 document in YAML; only clients parse YAML): the starter file of
3
+ * `shard templates init` and the export of a version (`shard templates export`), written like the dashboard's
4
+ * Template file section (keys in the recipe's order, long scripts as block literals, no line folding).
5
+ */
6
+ import { Document } from 'yaml';
7
+ /** A recipe as template.yaml, with `header` as the comment above the document. */
8
+ export function recipeYaml(recipe, header) {
9
+ const doc = new Document(recipe);
10
+ if (header)
11
+ doc.commentBefore = ` ${header}`;
12
+ return doc.toString({ lineWidth: 0, minContentWidth: 0, blockQuote: 'literal' });
13
+ }
14
+ /**
15
+ * The starter template.yaml on `base` (`<slug>@<version>`): every section present, with commented examples and one
16
+ * placeholder step (the API refuses a recipe that builds nothing). It parses to a valid recipe v2 and builds as it is.
17
+ */
18
+ export function initTemplateYaml(base) {
19
+ return `# A Shardflux template (recipe v2). Build it with: shard templates build template.yaml --wait
20
+ # build: what goes into the template (languages, packages, files, steps). settings: what a workspace gets when it opens.
21
+ schema: shardflux.template-recipe.v2
22
+ base: ${base}
23
+ build:
24
+ # Languages from the platform's table: python, node, go, rust, java (version omitted: the base's default).
25
+ languages: []
26
+ # - id: python
27
+ # - id: node
28
+ # version: "22"
29
+ packages:
30
+ apt: [] # e.g. [jq, ffmpeg]
31
+ pip:
32
+ packages: [] # installed into /opt/venv, e.g. [pandas==2.3.2]
33
+ requirements: [] # absolute paths of requirements files copied in by files, e.g. [/home/user/app/requirements.txt]
34
+ npm: [] # installed globally, e.g. [typescript@5.9.2]
35
+ # Local files and folders ("from", relative to this file) are uploaded by shard templates build; a folder is copied
36
+ # into "to" (kind tar), a file to the path "to" (kind file).
37
+ files: []
38
+ # - from: ./app
39
+ # to: /home/user/app
40
+ # owner: user
41
+ # - from: ./config/settings.toml
42
+ # to: /home/user/.config/app/settings.toml
43
+ # owner: user
44
+ # mode: "0600"
45
+ # Build steps run after languages, packages and files, in this order (as root unless user is set). A recipe must
46
+ # build something (a language, a package, a file or a step): replace this one with your own.
47
+ steps:
48
+ - name: hello
49
+ run: echo "built from template.yaml" > /etc/motd
50
+ # - name: install-deps
51
+ # run: npm ci
52
+ # user: user
53
+ # cwd: /home/user/app
54
+ network:
55
+ build: auto # auto: the hosts the recipe needs (plus extra_hosts); none; or allowlist with allow_hosts
56
+ settings:
57
+ env: {} # e.g. {APP_ENV: development}
58
+ inputs: {} # open-time inputs, e.g. {PROJECT_NAME: {kind: text, required: true}, OPENAI_API_KEY: {kind: secret}}
59
+ start: [] # start commands, e.g. [{name: seed, when: create, run: "python seed.py", user: user}]
60
+ services: {} # e.g. {web: {run: "npm start", user: user, cwd: /home/user/app, ready: {port: 3000}}}
61
+ `;
62
+ }
package/dist/timing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LifecycleTiming, ProgressListener } from '@shardflux/sdk';
1
+ import type { BuildFromFileEvent, LifecycleTiming, ProgressListener, TemplateBuild } from '@shardflux/sdk';
2
2
  export declare class TimingRecorder {
3
3
  /** The timing of the last traced call that ended (token fetches are not lifecycle calls and are skipped). */
4
4
  last: LifecycleTiming | null;
@@ -15,3 +15,31 @@ export declare class TimingRecorder {
15
15
  export declare function timingText(t: LifecycleTiming): string;
16
16
  /** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
17
17
  export declare function timingJson(t: LifecycleTiming | null): Record<string, unknown> | null;
18
+ /**
19
+ * Where a template build's time went, from the SDK's build-from-file progress events: reading and packing the local
20
+ * files, the uploads, the build request, and the wait; plus the build's own queued and run time from its view
21
+ * (created_at, started_at, completed_at).
22
+ */
23
+ export declare class BuildTimer {
24
+ #private;
25
+ packMs: number;
26
+ uploadMs: number;
27
+ files: number;
28
+ sent: number;
29
+ bytesSent: number;
30
+ buildId: string | null;
31
+ record(e: BuildFromFileEvent): void;
32
+ finish(build: TemplateBuild | null): void;
33
+ get totalMs(): number;
34
+ /** From the last upload (or the start, reading the file) to the created build: the organization lookup and the POST. */
35
+ get requestMs(): number | null;
36
+ get waitMs(): number | null;
37
+ get server(): {
38
+ queuedMs: number | null;
39
+ runMs: number | null;
40
+ };
41
+ }
42
+ /** One stderr line per build-from-file progress event. */
43
+ export declare function progressLine(e: BuildFromFileEvent): string;
44
+ export declare function buildTimingText(t: BuildTimer): string;
45
+ export declare function buildTimingJson(t: BuildTimer): Record<string, unknown>;
package/dist/timing.js CHANGED
@@ -63,3 +63,99 @@ export function timingJson(t) {
63
63
  outside_server_ms: t.outsideServerMs,
64
64
  };
65
65
  }
66
+ // ---- `shard templates build --timing` (0.4.0) ---------------------------------------------------------------------------
67
+ /**
68
+ * Where a template build's time went, from the SDK's build-from-file progress events: reading and packing the local
69
+ * files, the uploads, the build request, and the wait; plus the build's own queued and run time from its view
70
+ * (created_at, started_at, completed_at).
71
+ */
72
+ export class BuildTimer {
73
+ #t0 = performance.now();
74
+ packMs = 0;
75
+ uploadMs = 0;
76
+ files = 0;
77
+ sent = 0;
78
+ bytesSent = 0;
79
+ buildId = null;
80
+ #requestDoneAt = null;
81
+ #prepareDoneAt = null;
82
+ #endAt = null;
83
+ #build = null;
84
+ record(e) {
85
+ const now = performance.now();
86
+ if (e.type === 'pack') {
87
+ this.packMs += e.ms;
88
+ this.files += 1;
89
+ this.#prepareDoneAt = now;
90
+ }
91
+ else if (e.type === 'upload') {
92
+ this.uploadMs += e.ms;
93
+ if (e.uploaded) {
94
+ this.sent += 1;
95
+ this.bytesSent += e.size;
96
+ }
97
+ this.#prepareDoneAt = now;
98
+ }
99
+ else if (this.#requestDoneAt === null) {
100
+ this.#requestDoneAt = now;
101
+ this.buildId = e.build.id;
102
+ this.#build = e.build;
103
+ }
104
+ else
105
+ this.#build = e.build;
106
+ }
107
+ finish(build) {
108
+ this.#endAt = performance.now();
109
+ if (build)
110
+ this.#build = build;
111
+ }
112
+ get totalMs() {
113
+ return (this.#endAt ?? performance.now()) - this.#t0;
114
+ }
115
+ /** From the last upload (or the start, reading the file) to the created build: the organization lookup and the POST. */
116
+ get requestMs() {
117
+ return this.#requestDoneAt === null ? null : this.#requestDoneAt - (this.#prepareDoneAt ?? this.#t0);
118
+ }
119
+ get waitMs() {
120
+ return this.#requestDoneAt === null || this.#endAt === null ? null : this.#endAt - this.#requestDoneAt;
121
+ }
122
+ get server() {
123
+ const b = this.#build;
124
+ const ms = (a, z) => (a && z ? Math.max(0, Date.parse(z) - Date.parse(a)) : null);
125
+ return { queuedMs: ms(b?.created_at, b?.started_at), runMs: ms(b?.started_at, b?.completed_at) };
126
+ }
127
+ }
128
+ const fmtMs = (ms) => (ms === null ? '-' : ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(2)} s`);
129
+ const short = (sha) => sha.slice(0, 12);
130
+ const bytes = (n) => (n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(2)} GiB`);
131
+ /** One stderr line per build-from-file progress event. */
132
+ export function progressLine(e) {
133
+ if (e.type === 'pack')
134
+ return e.kind === 'tar' && e.entries !== null ? `packed ${e.from} (${e.entries} entries, ${bytes(e.size)}, sha256 ${short(e.sha256)}) in ${fmtMs(e.ms)}\n` : `read ${e.from} (${bytes(e.size)}, sha256 ${short(e.sha256)}) in ${fmtMs(e.ms)}\n`;
135
+ if (e.type === 'upload')
136
+ return e.uploaded ? `uploaded sha256:${short(e.sha256)} (${bytes(e.size)}) in ${fmtMs(e.ms)}\n` : `sha256:${short(e.sha256)} is already uploaded\n`;
137
+ const b = e.build;
138
+ return `build ${b.id}: ${b.state}${b.state === 'published' ? `, registration ${b.registration.state}` : ''}${b.failure ? ` (${b.failure.code})` : ''}\n`;
139
+ }
140
+ export function buildTimingText(t) {
141
+ const s = t.server;
142
+ const uploads = t.files ? `, upload ${fmtMs(t.uploadMs)} (${t.sent} of ${t.files} sent, ${bytes(t.bytesSent)})` : '';
143
+ const lines = [`timing: templates build ${fmtMs(t.totalMs)}${t.buildId ? ` (build ${t.buildId})` : ''}`, ` pack ${fmtMs(t.packMs)}${uploads}, request ${fmtMs(t.requestMs)}${t.waitMs !== null && t.waitMs > 1 ? `, wait ${fmtMs(t.waitMs)}` : ''}`];
144
+ if (s.queuedMs !== null || s.runMs !== null)
145
+ lines.push(` server: queued ${fmtMs(s.queuedMs)}, run ${fmtMs(s.runMs)}`);
146
+ return `${lines.join('\n')}\n`;
147
+ }
148
+ export function buildTimingJson(t) {
149
+ const r = (v) => (v === null ? null : Math.round(v * 10) / 10);
150
+ return {
151
+ action: 'template_build',
152
+ build_id: t.buildId,
153
+ total_ms: r(t.totalMs),
154
+ pack_ms: r(t.packMs),
155
+ upload_ms: r(t.uploadMs),
156
+ request_ms: r(t.requestMs),
157
+ wait_ms: r(t.waitMs),
158
+ uploads: { files: t.files, sent: t.sent, bytes_sent: t.bytesSent },
159
+ server: { queued_ms: t.server.queuedMs, run_ms: t.server.runMs },
160
+ };
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,8 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@shardflux/sdk": "^0.6.1"
42
+ "yaml": "2.9.1",
43
+ "@shardflux/sdk": "^0.7.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@eslint/js": "10.0.1",