@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.
- package/CHANGELOG.md +47 -1
- package/README.md +79 -7
- package/dist/args.d.ts +1 -1
- package/dist/args.js +124 -7
- package/dist/commands.js +330 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +18 -4
- package/dist/format.d.ts +19 -2
- package/dist/format.js +97 -7
- package/dist/main.js +3 -0
- package/dist/template-yaml.d.ts +7 -0
- package/dist/template-yaml.js +62 -0
- package/dist/timing.d.ts +29 -1
- package/dist/timing.js +96 -0
- package/package.json +3 -2
|
@@ -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
|
+
"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
|
-
"
|
|
42
|
+
"yaml": "2.9.1",
|
|
43
|
+
"@shardflux/sdk": "^0.7.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@eslint/js": "10.0.1",
|