@skrr-ai/cli 0.1.87 → 0.1.88
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/commands/agents/chat.js +27 -1
- package/dist/commands/computer/index.d.ts +19 -0
- package/dist/commands/computer/index.js +37 -0
- package/dist/commands/labels/create.d.ts +1 -0
- package/dist/commands/labels/create.js +2 -0
- package/dist/commands/labels/restore.d.ts +1 -0
- package/dist/commands/labels/restore.js +6 -3
- package/dist/commands/labels/update.d.ts +1 -0
- package/dist/commands/labels/update.js +2 -0
- package/dist/commands/machines/computer-adoption.d.ts +15 -0
- package/dist/commands/machines/computer-adoption.js +33 -0
- package/dist/commands/machines/dedicated/archive.d.ts +26 -0
- package/dist/commands/machines/dedicated/archive.js +113 -0
- package/dist/commands/machines/dedicated/audit.d.ts +25 -0
- package/dist/commands/machines/dedicated/audit.js +113 -0
- package/dist/commands/machines/dedicated/computer.d.ts +23 -0
- package/dist/commands/machines/dedicated/computer.js +64 -0
- package/dist/commands/machines/dedicated/cp.d.ts +2 -0
- package/dist/commands/machines/dedicated/cp.js +74 -2
- package/dist/commands/machines/dedicated/git-status.d.ts +15 -0
- package/dist/commands/machines/dedicated/git-status.js +56 -0
- package/dist/commands/machines/dedicated/index.js +15 -1
- package/dist/commands/machines/dedicated/ls.d.ts +27 -0
- package/dist/commands/machines/dedicated/ls.js +100 -0
- package/dist/commands/machines/dedicated/mkdir.d.ts +18 -0
- package/dist/commands/machines/dedicated/mkdir.js +57 -0
- package/dist/commands/machines/dedicated/mv.d.ts +24 -0
- package/dist/commands/machines/dedicated/mv.js +83 -0
- package/dist/commands/machines/dedicated/rm.d.ts +26 -0
- package/dist/commands/machines/dedicated/rm.js +100 -0
- package/dist/commands/machines/dedicated/search.d.ts +24 -0
- package/dist/commands/machines/dedicated/search.js +78 -0
- package/dist/commands/machines/dedicated/stat.d.ts +22 -0
- package/dist/commands/machines/dedicated/stat.js +92 -0
- package/dist/commands/machines/hosted/index.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/start.js +1 -1
- package/dist/commands/machines/services/declare.d.ts +24 -0
- package/dist/commands/machines/services/declare.js +73 -0
- package/dist/commands/machines/services/index.d.ts +15 -0
- package/dist/commands/machines/services/index.js +31 -0
- package/dist/commands/machines/services/ls.d.ts +17 -0
- package/dist/commands/machines/services/ls.js +60 -0
- package/dist/commands/machines/services/withdraw.d.ts +19 -0
- package/dist/commands/machines/services/withdraw.js +47 -0
- package/dist/commands/machines/share.d.ts +29 -0
- package/dist/commands/machines/share.js +100 -0
- package/dist/commands/machines/shared.d.ts +19 -0
- package/dist/commands/machines/shared.js +62 -0
- package/dist/commands/machines/shares.d.ts +16 -0
- package/dist/commands/machines/shares.js +69 -0
- package/dist/commands/machines/unshare.d.ts +19 -0
- package/dist/commands/machines/unshare.js +61 -0
- package/dist/commands/tasks/labels/create.d.ts +1 -0
- package/dist/commands/tasks/labels/create.js +3 -0
- package/dist/commands/views/create.d.ts +1 -0
- package/dist/commands/views/create.js +5 -0
- package/dist/lib/agent-home-workspace.d.ts +7 -0
- package/dist/lib/agent-home-workspace.js +147 -0
- package/dist/lib/computer-adoption.d.ts +55 -0
- package/dist/lib/computer-adoption.js +67 -0
- package/dist/lib/computer-consent.d.ts +13 -0
- package/dist/lib/computer-consent.js +33 -0
- package/dist/lib/computer-files.d.ts +239 -0
- package/dist/lib/computer-files.js +707 -0
- package/dist/lib/dedicated-copy.d.ts +45 -9
- package/dist/lib/dedicated-copy.js +141 -44
- package/dist/lib/dedicated-machines.d.ts +49 -0
- package/dist/lib/dedicated-machines.js +97 -6
- package/dist/lib/label-scope.d.ts +12 -0
- package/dist/lib/label-scope.js +15 -1
- package/dist/lib/machine-audit.d.ts +34 -0
- package/dist/lib/machine-audit.js +50 -0
- package/dist/lib/machine-grants.d.ts +70 -0
- package/dist/lib/machine-grants.js +47 -0
- package/dist/lib/machine-services.d.ts +64 -0
- package/dist/lib/machine-services.js +60 -0
- package/dist/lib/task-view-render.d.ts +2 -0
- package/dist/lib/task-view-render.js +1 -1
- package/dist/lib/views/vocabulary.d.ts +1 -1
- package/dist/lib/views/vocabulary.js +2 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +955 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +1070 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +955 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +1051 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +22143 -20728
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +11550 -9963
- package/package.json +2 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComputerFileEntityTag } from '@skrr-ai/data-provider';
|
|
2
|
+
import { statComputerFile } from './computer-files';
|
|
1
3
|
import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type DedicatedRuntimeScope } from './dedicated-machines';
|
|
2
4
|
/**
|
|
3
5
|
* The two copy loops behind `skrr machines dedicated cp`.
|
|
@@ -8,6 +10,14 @@ import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type Ded
|
|
|
8
10
|
* stages beside its destination on the machine and the final chunk renames it;
|
|
9
11
|
* a download writes a temporary file beside its local destination and renames.
|
|
10
12
|
*
|
|
13
|
+
* Every chunk presents the ONE lease generation the command resolved before it
|
|
14
|
+
* started (review C-01), so a machine replaced mid-copy refuses the remaining
|
|
15
|
+
* chunks instead of receiving them. And an upload never replaces a destination
|
|
16
|
+
* it was not told to (review CW-01): it stats the destination first, creates a
|
|
17
|
+
* new file only if the name is still free at commit (`createOnly`), and
|
|
18
|
+
* replaces an existing one only when asked to — and then only the exact version
|
|
19
|
+
* it saw (`replaceTag`).
|
|
20
|
+
*
|
|
11
21
|
* And neither leaves its partial behind when it is cancelled (OSK-8708).
|
|
12
22
|
* `listenForCopyInterrupt` turns SIGINT, SIGTERM and SIGHUP into an
|
|
13
23
|
* AbortSignal the loops honour; a download also removes its temporary file from
|
|
@@ -16,12 +26,25 @@ import { readDedicatedRuntimeFileChunk, writeDedicatedRuntimeFileChunk, type Ded
|
|
|
16
26
|
export interface DedicatedCopyIo {
|
|
17
27
|
readChunk: typeof readDedicatedRuntimeFileChunk;
|
|
18
28
|
writeChunk: typeof writeDedicatedRuntimeFileChunk;
|
|
29
|
+
/** How an upload learns what its destination is before it sends a byte. */
|
|
30
|
+
statPath: typeof statComputerFile;
|
|
19
31
|
chunkBytes: number;
|
|
20
32
|
maxBytes: number;
|
|
21
33
|
/** Run `fn` if the process exits; returns the unregister. Injectable for tests. */
|
|
22
34
|
onProcessExit: (fn: () => void) => () => void;
|
|
23
35
|
}
|
|
24
36
|
export interface DedicatedCopyOptions extends Partial<DedicatedCopyIo> {
|
|
37
|
+
/**
|
|
38
|
+
* The lease generation the command resolved at its start
|
|
39
|
+
* (`resolveDedicatedFileGeneration`), presented on every call of the copy.
|
|
40
|
+
*/
|
|
41
|
+
generation: number;
|
|
42
|
+
/** Upload: replace an existing destination (the version the stat found). */
|
|
43
|
+
overwrite?: boolean;
|
|
44
|
+
/** Upload: replace exactly this version of the destination, and no other. */
|
|
45
|
+
ifTag?: ComputerFileEntityTag;
|
|
46
|
+
/** Upload: told which version is about to be replaced, before any byte is sent. */
|
|
47
|
+
onReplace?: (destination: string, tag: ComputerFileEntityTag) => void;
|
|
25
48
|
/** Cancels the copy, which then removes its partial file before it rejects. */
|
|
26
49
|
signal?: AbortSignal;
|
|
27
50
|
/** Called after every chunk with the bytes copied so far and the file's size. */
|
|
@@ -35,8 +58,8 @@ export interface DedicatedCopyOptions extends Partial<DedicatedCopyIo> {
|
|
|
35
58
|
* (OSK-8714). Before, these local refusals had no code at all.
|
|
36
59
|
*/
|
|
37
60
|
export declare class DedicatedCopyError extends Error {
|
|
38
|
-
readonly code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED';
|
|
39
|
-
constructor(code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', message: string);
|
|
61
|
+
readonly code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_PATH_IS_DIRECTORY' | 'DEDICATED_RUNTIME_FILE_EXISTS' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED';
|
|
62
|
+
constructor(code: 'DEDICATED_RUNTIME_FILE_TOO_LARGE' | 'DEDICATED_RUNTIME_FILE_CHANGED' | 'DEDICATED_RUNTIME_FILE_NOT_FOUND' | 'DEDICATED_RUNTIME_PATH_NOT_A_FILE' | 'DEDICATED_RUNTIME_PATH_IS_DIRECTORY' | 'DEDICATED_RUNTIME_FILE_EXISTS' | 'DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', message: string);
|
|
40
63
|
}
|
|
41
64
|
declare const SIGNAL_NUMBERS: {
|
|
42
65
|
readonly SIGHUP: 1;
|
|
@@ -76,6 +99,14 @@ export declare function listenForCopyInterrupt({ proc, notice, }?: {
|
|
|
76
99
|
export interface DedicatedCopyResult {
|
|
77
100
|
bytes: number;
|
|
78
101
|
destination: string;
|
|
102
|
+
/** Upload: true when the copy created the destination, false when it replaced one. */
|
|
103
|
+
created?: boolean;
|
|
104
|
+
/** Upload: the tag of the version that was replaced, as `stat` prints it. */
|
|
105
|
+
replacedTag?: string;
|
|
106
|
+
/** Upload: the committed file's tag, as `stat` prints it. */
|
|
107
|
+
tag?: string;
|
|
108
|
+
/** Upload: the machine answered from the receipt of an earlier, committed attempt. */
|
|
109
|
+
alreadyCommitted?: boolean;
|
|
79
110
|
}
|
|
80
111
|
/**
|
|
81
112
|
* One line of progress for a terminal, redrawn in place: a 60 MiB copy takes
|
|
@@ -100,13 +131,18 @@ export declare function intoRemoteDirectory(directory: string, name: string): st
|
|
|
100
131
|
* Upload one file. A destination that is a directory — spelled `dir/`, or an
|
|
101
132
|
* existing directory named without the slash — receives the file under its own
|
|
102
133
|
* name, as `cp` and `scp` do and as a download into a local directory already
|
|
103
|
-
* did
|
|
104
|
-
*
|
|
134
|
+
* did (OSK-8713).
|
|
135
|
+
*
|
|
136
|
+
* The destination is stat'ed before any byte is sent, and what the copy may do
|
|
137
|
+
* to it is decided then and enforced at commit by the machine (review CW-01):
|
|
105
138
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
139
|
+
* - nothing there → the copy creates it, and commits only if the name is still
|
|
140
|
+
* free (`createOnly`); something that appears meanwhile is never replaced;
|
|
141
|
+
* - a file there → refused before sending anything, unless `overwrite` (replace
|
|
142
|
+
* the version the stat found) or `ifTag` (replace exactly that version) was
|
|
143
|
+
* given; either way the commit replaces only that version (`replaceTag`), and
|
|
144
|
+
* a file changed during the copy is refused `file_changed`, nothing replaced.
|
|
109
145
|
*/
|
|
110
|
-
export declare function uploadToDedicatedRuntime(localPath: string, leaseId: string, remotePath: string, scope
|
|
111
|
-
export declare function downloadFromDedicatedRuntime(leaseId: string, remotePath: string, localPath: string, scope
|
|
146
|
+
export declare function uploadToDedicatedRuntime(localPath: string, leaseId: string, remotePath: string, scope: DedicatedRuntimeScope, io: DedicatedCopyOptions): Promise<DedicatedCopyResult>;
|
|
147
|
+
export declare function downloadFromDedicatedRuntime(leaseId: string, remotePath: string, localPath: string, scope: DedicatedRuntimeScope, io: DedicatedCopyOptions): Promise<DedicatedCopyResult>;
|
|
112
148
|
export {};
|
|
@@ -12,6 +12,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
12
12
|
const node_fs_1 = require("node:fs");
|
|
13
13
|
const promises_1 = require("node:fs/promises");
|
|
14
14
|
const node_path_1 = require("node:path");
|
|
15
|
+
const computer_files_1 = require("./computer-files");
|
|
15
16
|
const dedicated_file_retry_1 = require("./dedicated-file-retry");
|
|
16
17
|
const dedicated_machines_1 = require("./dedicated-machines");
|
|
17
18
|
/**
|
|
@@ -34,6 +35,7 @@ const changedWhileCopying = (path, where) => new DedicatedCopyError('DEDICATED_R
|
|
|
34
35
|
const defaultIo = {
|
|
35
36
|
readChunk: dedicated_machines_1.readDedicatedRuntimeFileChunk,
|
|
36
37
|
writeChunk: dedicated_machines_1.writeDedicatedRuntimeFileChunk,
|
|
38
|
+
statPath: computer_files_1.statComputerFile,
|
|
37
39
|
chunkBytes: dedicated_machines_1.DEDICATED_FILE_CHUNK_BYTES,
|
|
38
40
|
maxBytes: dedicated_machines_1.DEDICATED_FILE_MAX_BYTES,
|
|
39
41
|
onProcessExit: (fn) => {
|
|
@@ -138,51 +140,107 @@ function intoRemoteDirectory(directory, name) {
|
|
|
138
140
|
const parent = directory.replace(/\/\.?$/, '').replace(/^\.$/, '');
|
|
139
141
|
return parent ? `${parent}/${name}` : name;
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Stat one machine path for an upload. A path that does not exist is `absent`
|
|
145
|
+
* — the copy creates it, and any missing parent with it. Every other refusal
|
|
146
|
+
* (withheld, stale generation, the machine unreachable) is the copy's refusal.
|
|
147
|
+
*/
|
|
148
|
+
async function statDestination(statPath, leaseId, remotePath, generation, scope, signal) {
|
|
149
|
+
let result;
|
|
150
|
+
try {
|
|
151
|
+
result = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => statPath(leaseId, { path: remotePath, generation }, scope), signal);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const refusal = (0, computer_files_1.computerFileRefusalInfo)(err);
|
|
155
|
+
// `details.path` (or a path-shaped message) marks a missing PATH; without
|
|
156
|
+
// it the machine itself was not found, which is not a destination to create.
|
|
157
|
+
if (refusal?.code === 'not_found' &&
|
|
158
|
+
(refusal.details?.path !== undefined || /path|file|folder|directory/i.test(refusal.message))) {
|
|
159
|
+
return { kind: 'absent' };
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const type = result.entry.type;
|
|
164
|
+
if (type === 'directory')
|
|
165
|
+
return { kind: 'directory' };
|
|
166
|
+
if (type === 'file' && result.tag) {
|
|
167
|
+
return {
|
|
168
|
+
kind: 'file',
|
|
169
|
+
tag: { inode: result.tag.inode, size: result.tag.size, mtimeNs: result.tag.mtimeNs },
|
|
170
|
+
};
|
|
148
171
|
}
|
|
172
|
+
return { kind: 'other', type };
|
|
149
173
|
}
|
|
174
|
+
const sameTag = (a, b) => a.inode === b.inode && a.size === b.size && a.mtimeNs === b.mtimeNs;
|
|
150
175
|
/**
|
|
151
176
|
* Upload one file. A destination that is a directory — spelled `dir/`, or an
|
|
152
177
|
* existing directory named without the slash — receives the file under its own
|
|
153
178
|
* name, as `cp` and `scp` do and as a download into a local directory already
|
|
154
|
-
* did
|
|
155
|
-
* failed the machine's final rename with EISDIR (OSK-8713).
|
|
179
|
+
* did (OSK-8713).
|
|
156
180
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
181
|
+
* The destination is stat'ed before any byte is sent, and what the copy may do
|
|
182
|
+
* to it is decided then and enforced at commit by the machine (review CW-01):
|
|
183
|
+
*
|
|
184
|
+
* - nothing there → the copy creates it, and commits only if the name is still
|
|
185
|
+
* free (`createOnly`); something that appears meanwhile is never replaced;
|
|
186
|
+
* - a file there → refused before sending anything, unless `overwrite` (replace
|
|
187
|
+
* the version the stat found) or `ifTag` (replace exactly that version) was
|
|
188
|
+
* given; either way the commit replaces only that version (`replaceTag`), and
|
|
189
|
+
* a file changed during the copy is refused `file_changed`, nothing replaced.
|
|
160
190
|
*/
|
|
161
|
-
async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope, io) {
|
|
192
|
+
const { statPath, signal, generation } = { ...defaultIo, ...io };
|
|
193
|
+
throwIfCancelled(signal);
|
|
194
|
+
const info = await localSourceInfo(localPath, { ...defaultIo, ...io }.maxBytes);
|
|
195
|
+
const shown = (target) => `${leaseId}:${target}`;
|
|
196
|
+
let target = namesRemoteDirectory(remotePath)
|
|
197
|
+
? intoRemoteDirectory(remotePath, (0, node_path_1.basename)(localPath))
|
|
198
|
+
: remotePath;
|
|
199
|
+
let destination = await statDestination(statPath, leaseId, target, generation, scope, signal);
|
|
200
|
+
if (destination.kind === 'directory' && target === remotePath) {
|
|
201
|
+
// An existing directory named without the slash: into it, once.
|
|
202
|
+
target = intoRemoteDirectory(remotePath, (0, node_path_1.basename)(localPath));
|
|
203
|
+
destination = await statDestination(statPath, leaseId, target, generation, scope, signal);
|
|
204
|
+
}
|
|
205
|
+
if (destination.kind === 'directory') {
|
|
206
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_PATH_IS_DIRECTORY', `${shown(target)} is a directory on the machine; nothing was sent.`);
|
|
207
|
+
}
|
|
208
|
+
if (destination.kind === 'other') {
|
|
209
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_PATH_NOT_A_FILE', `${shown(target)} is a ${destination.type} on the machine, not a regular file; cp does not replace it. Nothing was sent.`);
|
|
210
|
+
}
|
|
211
|
+
let condition;
|
|
212
|
+
let replacedTag;
|
|
213
|
+
if (destination.kind === 'absent') {
|
|
214
|
+
if (io.ifTag) {
|
|
215
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shown(target)} is no longer there, so the version --if-tag ${(0, computer_files_1.computerFileTagString)(io.ifTag)} names cannot be replaced. Nothing was sent; copy again without --if-tag to create it.`);
|
|
166
216
|
}
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
condition = { createOnly: true };
|
|
218
|
+
}
|
|
219
|
+
else if (io.ifTag) {
|
|
220
|
+
if (!sameTag(io.ifTag, destination.tag)) {
|
|
221
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shown(target)} changed since --if-tag ${(0, computer_files_1.computerFileTagString)(io.ifTag)} was read; its current tag is ${(0, computer_files_1.computerFileTagString)(destination.tag)}. Nothing was sent or replaced.`);
|
|
169
222
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return attempt(intoDirectory(remotePath));
|
|
223
|
+
replacedTag = destination.tag;
|
|
224
|
+
condition = { replaceTag: destination.tag };
|
|
173
225
|
}
|
|
174
|
-
|
|
175
|
-
|
|
226
|
+
else if (io.overwrite) {
|
|
227
|
+
replacedTag = destination.tag;
|
|
228
|
+
condition = { replaceTag: destination.tag };
|
|
176
229
|
}
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
return attempt(intoDirectory(remotePath));
|
|
180
|
-
throw err;
|
|
230
|
+
else {
|
|
231
|
+
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_EXISTS', `${shown(target)} already exists (tag ${(0, computer_files_1.computerFileTagString)(destination.tag)}). Nothing was sent. Pass --overwrite to replace it, or --if-tag ${(0, computer_files_1.computerFileTagString)(destination.tag)} to replace exactly this version.`);
|
|
181
232
|
}
|
|
233
|
+
if (replacedTag)
|
|
234
|
+
io.onReplace?.(shown(target), replacedTag);
|
|
235
|
+
const result = await uploadFile(localPath, info, leaseId, target, scope, io, condition);
|
|
236
|
+
return {
|
|
237
|
+
...result,
|
|
238
|
+
created: replacedTag === undefined,
|
|
239
|
+
...(replacedTag ? { replacedTag: (0, computer_files_1.computerFileTagString)(replacedTag) } : {}),
|
|
240
|
+
};
|
|
182
241
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
throwIfCancelled(signal);
|
|
242
|
+
/** The local source of an upload: a regular file within the size limit. */
|
|
243
|
+
async function localSourceInfo(localPath, maxBytes) {
|
|
186
244
|
const info = await (0, promises_1.stat)(localPath).catch((err) => {
|
|
187
245
|
// The raw `ENOENT: no such file or directory, stat './x'` said the same
|
|
188
246
|
// thing in the platform's words rather than ours.
|
|
@@ -197,6 +255,34 @@ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
|
|
|
197
255
|
if (info.size > maxBytes) {
|
|
198
256
|
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_TOO_LARGE', `${localPath} is ${mib(info.size)}; copy is limited to ${mib(maxBytes)}. Push it through Git or object storage instead.`);
|
|
199
257
|
}
|
|
258
|
+
return info;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The commit's own refusal of the condition the upload carried, in the copy's
|
|
262
|
+
* words: the destination changed (or appeared) DURING the copy and nothing was
|
|
263
|
+
* replaced. Anything else passes through unchanged.
|
|
264
|
+
*/
|
|
265
|
+
function commitConditionRefusal(err, shownTarget) {
|
|
266
|
+
const refusal = (0, computer_files_1.computerFileRefusalInfo)(err);
|
|
267
|
+
if (!refusal)
|
|
268
|
+
return err;
|
|
269
|
+
if (refusal.code === 'file_changed') {
|
|
270
|
+
let now = '';
|
|
271
|
+
if (refusal.currentTag !== undefined) {
|
|
272
|
+
now = ` Its current tag is ${(0, computer_files_1.computerFileTagString)(refusal.currentTag)}.`;
|
|
273
|
+
}
|
|
274
|
+
else if (refusal.body.currentTag === null) {
|
|
275
|
+
now = ' It is no longer there.';
|
|
276
|
+
}
|
|
277
|
+
return new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${shownTarget} changed on the machine during the copy, so nothing was replaced.${now} Look at it with \`stat\`, then copy again.`);
|
|
278
|
+
}
|
|
279
|
+
if (refusal.code === 'invalid_request' && refusal.details?.reason === 'exists') {
|
|
280
|
+
return new DedicatedCopyError('DEDICATED_RUNTIME_FILE_EXISTS', `${shownTarget} appeared on the machine during the copy, so nothing was replaced. Copy again with --overwrite if you mean to replace it.`);
|
|
281
|
+
}
|
|
282
|
+
return err;
|
|
283
|
+
}
|
|
284
|
+
async function uploadFile(localPath, info, leaseId, remotePath, scope, io, condition) {
|
|
285
|
+
const { writeChunk, chunkBytes, signal, onProgress, generation } = { ...defaultIo, ...io };
|
|
200
286
|
const transferId = `cp-${Date.now().toString(36)}-${(0, node_crypto_1.randomBytes)(6).toString('hex')}`;
|
|
201
287
|
const handle = await (0, promises_1.open)(localPath, 'r');
|
|
202
288
|
let offset = 0;
|
|
@@ -217,36 +303,45 @@ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
|
|
|
217
303
|
path: remotePath,
|
|
218
304
|
transferId,
|
|
219
305
|
offset,
|
|
306
|
+
generation,
|
|
220
307
|
dataBase64: buffer.subarray(0, bytesRead).toString('base64'),
|
|
308
|
+
// On every chunk, so a machine whose image cannot honour the
|
|
309
|
+
// condition refuses the FIRST chunk, before anything is staged.
|
|
310
|
+
...condition,
|
|
221
311
|
// A file that GREW is caught here: the machine refuses to commit
|
|
222
312
|
// when what it staged is not the size declared at the start.
|
|
223
|
-
...(final
|
|
313
|
+
...(final
|
|
314
|
+
? { final: true, totalSize: info.size, executable: (info.mode & 0o111) !== 0 }
|
|
315
|
+
: {}),
|
|
224
316
|
}, scope), signal);
|
|
225
317
|
offset += bytesRead;
|
|
226
318
|
onProgress?.(offset, info.size);
|
|
227
|
-
|
|
228
|
-
|
|
319
|
+
// A receipt (`alreadyCommitted`) is the same success: this transfer
|
|
320
|
+
// committed earlier and only its acknowledgement was lost.
|
|
321
|
+
if (final || result.alreadyCommitted === true) {
|
|
322
|
+
return {
|
|
323
|
+
bytes: info.size,
|
|
324
|
+
destination: `${leaseId}:${result.path || remotePath}`,
|
|
325
|
+
...(result.tag ? { tag: (0, computer_files_1.computerFileTagString)(result.tag) } : {}),
|
|
326
|
+
...(result.alreadyCommitted === true ? { alreadyCommitted: true } : {}),
|
|
327
|
+
};
|
|
229
328
|
}
|
|
230
329
|
}
|
|
231
330
|
}
|
|
232
331
|
catch (err) {
|
|
233
|
-
if (offset === 0 && err?.code === PATH_IS_DIRECTORY) {
|
|
234
|
-
// Refused before the first chunk was staged: there is nothing to abort.
|
|
235
|
-
throw new DestinationIsDirectory(err);
|
|
236
|
-
}
|
|
237
332
|
// Best effort: remove the staged partial so it does not sit beside the
|
|
238
333
|
// destination. The destination itself was never touched. An abort that
|
|
239
334
|
// never arrives — the process killed, the network gone — is covered by the
|
|
240
335
|
// machine, which removes a stale staging file on the next upload into the
|
|
241
336
|
// same directory (daemon/src/tools/file-transfer.ts).
|
|
242
|
-
await writeChunk(leaseId, { path: remotePath, transferId, offset: 0, abort: true }, scope).catch(() => undefined);
|
|
243
|
-
throw err;
|
|
337
|
+
await writeChunk(leaseId, { path: remotePath, transferId, offset: 0, abort: true, generation }, scope).catch(() => undefined);
|
|
338
|
+
throw commitConditionRefusal(err, `${leaseId}:${remotePath}`);
|
|
244
339
|
}
|
|
245
340
|
finally {
|
|
246
341
|
await handle.close();
|
|
247
342
|
}
|
|
248
343
|
}
|
|
249
|
-
async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scope
|
|
344
|
+
async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scope, io) {
|
|
250
345
|
const { readChunk, chunkBytes, onProcessExit, signal, onProgress } = { ...defaultIo, ...io };
|
|
251
346
|
throwIfCancelled(signal);
|
|
252
347
|
let target = localPath;
|
|
@@ -282,14 +377,16 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
|
|
|
282
377
|
throwIfCancelled(signal);
|
|
283
378
|
// Nothing on the machine needs undoing, so an in-flight read is abandoned
|
|
284
379
|
// at once rather than waited out.
|
|
285
|
-
const chunk = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => readChunk(leaseId, { path: remotePath, offset, length: chunkBytes }, scope, signal), signal);
|
|
380
|
+
const chunk = await (0, dedicated_file_retry_1.withFileAdmissionRetry)(() => readChunk(leaseId, { path: remotePath, offset, length: chunkBytes, generation: io.generation }, scope, signal), signal);
|
|
286
381
|
if (size === -1) {
|
|
287
382
|
size = chunk.size;
|
|
288
383
|
mtimeMs = chunk.mtimeMs;
|
|
289
384
|
executable = chunk.executable;
|
|
290
385
|
}
|
|
291
|
-
else if (chunk.size !== size ||
|
|
292
|
-
|
|
386
|
+
else if (chunk.size !== size ||
|
|
387
|
+
chunk.mtimeMs !== mtimeMs ||
|
|
388
|
+
(typeof chunk.executable === 'boolean' &&
|
|
389
|
+
typeof executable === 'boolean' &&
|
|
293
390
|
chunk.executable !== executable)) {
|
|
294
391
|
// Chunks from two versions of a file splice into one that never existed.
|
|
295
392
|
throw changedWhileCopying(remotePath, 'on the machine');
|
|
@@ -285,6 +285,7 @@ export declare function dedicatedRuntimeCreateInput(input: {
|
|
|
285
285
|
* billable request is sent.
|
|
286
286
|
*/
|
|
287
287
|
export declare function isDedicatedRepositoryRef(value: string): boolean;
|
|
288
|
+
export declare function withDedicatedScope(path: string, scope: DedicatedRuntimeScope, query?: Record<string, string>): string;
|
|
288
289
|
export declare function createDedicatedRuntime(input: CreateDedicatedRuntimeInput): Promise<DedicatedRuntimeResponse>;
|
|
289
290
|
/** One offered tier as the operator price book checks it. */
|
|
290
291
|
export interface DedicatedPriceBookPresetCheck {
|
|
@@ -478,21 +479,69 @@ export interface DedicatedFileWriteResult {
|
|
|
478
479
|
staged: number;
|
|
479
480
|
committed: boolean;
|
|
480
481
|
aborted?: boolean;
|
|
482
|
+
/** On commit: the committed file's entity tag. */
|
|
483
|
+
tag?: {
|
|
484
|
+
inode: string;
|
|
485
|
+
size: number;
|
|
486
|
+
mtimeNs: string;
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* This transfer had ALREADY committed (review CW-02): the answer is its
|
|
490
|
+
* receipt, and nothing was written again — a retried final chunk whose first
|
|
491
|
+
* acknowledgement was lost. A success, never a reason to upload again.
|
|
492
|
+
*/
|
|
493
|
+
alreadyCommitted?: boolean;
|
|
481
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* `lease:<leaseId>` is the machine id the computer file routes address a
|
|
497
|
+
* Dedicated Runtime by (`/api/machines/:machineId/files/*`, authorised by
|
|
498
|
+
* ComputerAccess), the same id the machine inventory uses.
|
|
499
|
+
*/
|
|
500
|
+
export declare const computerMachineId: (leaseId: string) => string;
|
|
501
|
+
/**
|
|
502
|
+
* The computer's page for a Dedicated Runtime — `/machines/<machineId>/computer`
|
|
503
|
+
* (the route `paneRouteConfig` serves the computer shell at, design §11).
|
|
504
|
+
* Composed on the `machine` web route rather than concatenated against baseURL,
|
|
505
|
+
* so the `api.`-subdomain → apex origin mapping stays the one in web-url.ts.
|
|
506
|
+
*/
|
|
507
|
+
export declare function dedicatedComputerUrl(baseURL: string, leaseId: string): string | null;
|
|
508
|
+
/**
|
|
509
|
+
* One chunk of a download. `generation` is the lease generation the `cp`
|
|
510
|
+
* command resolved at its start, presented on every chunk (review C-01): a
|
|
511
|
+
* machine replaced mid-copy refuses the rest `stale_generation`.
|
|
512
|
+
*/
|
|
482
513
|
export declare function readDedicatedRuntimeFileChunk(leaseId: string, input: {
|
|
483
514
|
path: string;
|
|
484
515
|
offset: number;
|
|
485
516
|
length: number;
|
|
517
|
+
generation: number;
|
|
486
518
|
}, scope?: DedicatedRuntimeScope, signal?: AbortSignal): Promise<DedicatedFileChunk>;
|
|
519
|
+
/**
|
|
520
|
+
* One call of a chunked upload. `generation` fences every chunk to the machine
|
|
521
|
+
* the command resolved (review C-01). How the commit treats a destination is
|
|
522
|
+
* the caller's explicit choice (review CW-01), never an unconditional replace:
|
|
523
|
+
* `createOnly` commits only if nothing has the name (else 409
|
|
524
|
+
* `invalid_request`/`exists`); `replaceTag` commits only over the file with
|
|
525
|
+
* exactly that tag (else 409 `file_changed` with `currentTag`, null when it is
|
|
526
|
+
* gone). Never both. Each needs `computer_files_fenced_v1` on the machine.
|
|
527
|
+
*/
|
|
487
528
|
export declare function writeDedicatedRuntimeFileChunk(leaseId: string, input: {
|
|
488
529
|
path: string;
|
|
489
530
|
transferId: string;
|
|
490
531
|
offset: number;
|
|
532
|
+
generation: number;
|
|
491
533
|
dataBase64?: string;
|
|
492
534
|
final?: boolean;
|
|
493
535
|
abort?: boolean;
|
|
536
|
+
query?: boolean;
|
|
494
537
|
totalSize?: number;
|
|
495
538
|
executable?: boolean;
|
|
539
|
+
createOnly?: boolean;
|
|
540
|
+
replaceTag?: {
|
|
541
|
+
inode: string;
|
|
542
|
+
size: number;
|
|
543
|
+
mtimeNs: string;
|
|
544
|
+
};
|
|
496
545
|
}, scope?: DedicatedRuntimeScope): Promise<DedicatedFileWriteResult>;
|
|
497
546
|
export type DedicatedCopyPlan = {
|
|
498
547
|
direction: 'upload';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
3
|
+
exports.computerMachineId = exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
4
4
|
exports.describeDedicatedCheckpoint = describeDedicatedCheckpoint;
|
|
5
5
|
exports.compareDaemonVersions = compareDaemonVersions;
|
|
6
6
|
exports.describeDedicatedDaemonRelease = describeDedicatedDaemonRelease;
|
|
7
7
|
exports.describeDedicatedImageStanding = describeDedicatedImageStanding;
|
|
8
8
|
exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
|
|
9
9
|
exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
|
|
10
|
+
exports.withDedicatedScope = withDedicatedScope;
|
|
10
11
|
exports.createDedicatedRuntime = createDedicatedRuntime;
|
|
11
12
|
exports.getDedicatedOperatorPriceBook = getDedicatedOperatorPriceBook;
|
|
12
13
|
exports.dedicatedOperatorPriceBookLines = dedicatedOperatorPriceBookLines;
|
|
@@ -22,6 +23,7 @@ exports.execDedicatedRuntime = execDedicatedRuntime;
|
|
|
22
23
|
exports.getDedicatedRuntimeSignInStatus = getDedicatedRuntimeSignInStatus;
|
|
23
24
|
exports.startDedicatedRuntimeExecution = startDedicatedRuntimeExecution;
|
|
24
25
|
exports.observeDedicatedRuntimeExecution = observeDedicatedRuntimeExecution;
|
|
26
|
+
exports.dedicatedComputerUrl = dedicatedComputerUrl;
|
|
25
27
|
exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
|
|
26
28
|
exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
|
|
27
29
|
exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
|
|
@@ -53,6 +55,7 @@ const auth_core_1 = require("@skrr-ai/auth-core");
|
|
|
53
55
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
54
56
|
const api_fetch_1 = require("./api-fetch");
|
|
55
57
|
const machine_spend_cap_1 = require("./machine-spend-cap");
|
|
58
|
+
const web_url_1 = require("./web-url");
|
|
56
59
|
const edge_error_page_1 = require("./edge-error-page");
|
|
57
60
|
const harnesses_1 = require("./harnesses");
|
|
58
61
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
@@ -445,11 +448,89 @@ async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
|
|
|
445
448
|
/** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
|
|
446
449
|
exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
|
|
447
450
|
exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
|
|
451
|
+
/**
|
|
452
|
+
* `lease:<leaseId>` is the machine id the computer file routes address a
|
|
453
|
+
* Dedicated Runtime by (`/api/machines/:machineId/files/*`, authorised by
|
|
454
|
+
* ComputerAccess), the same id the machine inventory uses.
|
|
455
|
+
*/
|
|
456
|
+
const computerMachineId = (leaseId) => `lease:${leaseId}`;
|
|
457
|
+
exports.computerMachineId = computerMachineId;
|
|
458
|
+
/**
|
|
459
|
+
* The computer's page for a Dedicated Runtime — `/machines/<machineId>/computer`
|
|
460
|
+
* (the route `paneRouteConfig` serves the computer shell at, design §11).
|
|
461
|
+
* Composed on the `machine` web route rather than concatenated against baseURL,
|
|
462
|
+
* so the `api.`-subdomain → apex origin mapping stays the one in web-url.ts.
|
|
463
|
+
*/
|
|
464
|
+
function dedicatedComputerUrl(baseURL, leaseId) {
|
|
465
|
+
const page = (0, web_url_1.webUrl)(baseURL, { kind: 'machine', machineId: (0, exports.computerMachineId)(leaseId) });
|
|
466
|
+
return page ? `${page}/computer` : null;
|
|
467
|
+
}
|
|
468
|
+
const LEGACY_FILE_CODES = {
|
|
469
|
+
file_changed: 'DEDICATED_RUNTIME_FILE_CHANGED',
|
|
470
|
+
too_large: 'DEDICATED_RUNTIME_FILE_TOO_LARGE',
|
|
471
|
+
not_found: 'DEDICATED_RUNTIME_FILE_NOT_FOUND',
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* The copy loop branches on the `DEDICATED_RUNTIME_*` code a refused chunk
|
|
475
|
+
* carried (a destination that is a directory, a file that changed). The file
|
|
476
|
+
* routes answer in the computer's wire vocabulary and keep the specific cause in
|
|
477
|
+
* `details.reason`, so a refusal is re-coded here, at the one place that knows
|
|
478
|
+
* about the copy loop, rather than the loop learning two vocabularies.
|
|
479
|
+
*/
|
|
480
|
+
function withCopyLoopCode(err) {
|
|
481
|
+
if (!(err instanceof api_fetch_1.ApiFetchError))
|
|
482
|
+
return err;
|
|
483
|
+
let reason;
|
|
484
|
+
try {
|
|
485
|
+
reason = JSON.parse(err.body ?? '{}').details?.reason;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
reason = undefined;
|
|
489
|
+
}
|
|
490
|
+
const code = typeof reason === 'string' && reason.startsWith('DEDICATED_RUNTIME_')
|
|
491
|
+
? reason
|
|
492
|
+
: err.code
|
|
493
|
+
? LEGACY_FILE_CODES[err.code]
|
|
494
|
+
: undefined;
|
|
495
|
+
if (code)
|
|
496
|
+
err.code = code;
|
|
497
|
+
return err;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* One chunk of a download. `generation` is the lease generation the `cp`
|
|
501
|
+
* command resolved at its start, presented on every chunk (review C-01): a
|
|
502
|
+
* machine replaced mid-copy refuses the rest `stale_generation`.
|
|
503
|
+
*/
|
|
448
504
|
async function readDedicatedRuntimeFileChunk(leaseId, input, scope = {}, signal) {
|
|
449
|
-
|
|
505
|
+
const wire = await (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/${encodeURIComponent((0, exports.computerMachineId)(leaseId))}/files/read`, scope), { method: 'POST', body: input, ...(signal ? { signal } : {}) }).catch((err) => {
|
|
506
|
+
throw withCopyLoopCode(err);
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
path: wire.path,
|
|
510
|
+
size: wire.size,
|
|
511
|
+
// The tag's nanosecond mtime, as the whole-millisecond value the copy loop
|
|
512
|
+
// compares between chunks to notice a file that changed under it.
|
|
513
|
+
mtimeMs: Number(BigInt(wire.tag.mtimeNs) / 1000000n),
|
|
514
|
+
...(typeof wire.executable === 'boolean' ? { executable: wire.executable } : {}),
|
|
515
|
+
offset: wire.offset,
|
|
516
|
+
bytesRead: Buffer.from(wire.data, 'base64').byteLength,
|
|
517
|
+
dataBase64: wire.data,
|
|
518
|
+
eof: wire.eof,
|
|
519
|
+
};
|
|
450
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* One call of a chunked upload. `generation` fences every chunk to the machine
|
|
523
|
+
* the command resolved (review C-01). How the commit treats a destination is
|
|
524
|
+
* the caller's explicit choice (review CW-01), never an unconditional replace:
|
|
525
|
+
* `createOnly` commits only if nothing has the name (else 409
|
|
526
|
+
* `invalid_request`/`exists`); `replaceTag` commits only over the file with
|
|
527
|
+
* exactly that tag (else 409 `file_changed` with `currentTag`, null when it is
|
|
528
|
+
* gone). Never both. Each needs `computer_files_fenced_v1` on the machine.
|
|
529
|
+
*/
|
|
451
530
|
async function writeDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
|
|
452
|
-
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines
|
|
531
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/${encodeURIComponent((0, exports.computerMachineId)(leaseId))}/files/write`, scope), { method: 'POST', body: input }).catch((err) => {
|
|
532
|
+
throw withCopyLoopCode(err);
|
|
533
|
+
});
|
|
453
534
|
}
|
|
454
535
|
const REMOTE_ENDPOINT = /^(rtl_[A-Za-z0-9_-]{1,80}):(.+)$/;
|
|
455
536
|
/**
|
|
@@ -570,7 +651,8 @@ function dedicatedSpendSummary(lease) {
|
|
|
570
651
|
if (!billing)
|
|
571
652
|
return '-';
|
|
572
653
|
const cap = billing.monthlySpendLimitCents !== undefined
|
|
573
|
-
? billing.monthlySpendLimitCents
|
|
654
|
+
? billing.monthlySpendLimitCents
|
|
655
|
+
: billing.spendingLimitCents;
|
|
574
656
|
if (billing.spendStatusUnavailable) {
|
|
575
657
|
return `spend unavailable right now · ${cap === null ? 'no monthly cap' : `cap ${formatUsdCents(cap)}`}`;
|
|
576
658
|
}
|
|
@@ -647,6 +729,12 @@ function describeDedicatedLease(lease, harnesses = [], opts = {}) {
|
|
|
647
729
|
row('state', lease.stateReason
|
|
648
730
|
? `${dedicatedLeaseState(lease)} (${lease.stateReason})`
|
|
649
731
|
: dedicatedLeaseState(lease));
|
|
732
|
+
// A reason the platform wrote for its own stop gets a sentence, not just a code: the
|
|
733
|
+
// code names an internal rule and leaves the owner asking where their machine went
|
|
734
|
+
// (OSK-13732). A reason with no copy keeps printing as the code above.
|
|
735
|
+
const stateReasonCopy = (0, data_provider_1.describeMachineStateReason)(lease.stateReason);
|
|
736
|
+
if (stateReasonCopy)
|
|
737
|
+
row('why', stateReasonCopy);
|
|
650
738
|
const resources = lease.resources || {};
|
|
651
739
|
const size = [
|
|
652
740
|
resources.sizePreset,
|
|
@@ -1119,10 +1207,13 @@ function dedicatedCreateSummary(catalog, choices) {
|
|
|
1119
1207
|
: 'server default';
|
|
1120
1208
|
const retains = retentionClass !== undefined && retentionClass !== 'none';
|
|
1121
1209
|
const capCents = choices.spendingLimitCents !== undefined
|
|
1122
|
-
? choices.spendingLimitCents
|
|
1210
|
+
? choices.spendingLimitCents
|
|
1211
|
+
: catalog?.defaultMonthlySpendingLimitCents;
|
|
1123
1212
|
const cap = capCents !== undefined && capCents !== null
|
|
1124
1213
|
? `${formatUsdCents(capCents)} per month${choices.spendingLimitCents === undefined ? ' (catalog default)' : ''}`
|
|
1125
|
-
: capCents === null
|
|
1214
|
+
: capCents === null
|
|
1215
|
+
? 'no cap — compute and retained storage still bill'
|
|
1216
|
+
: 'server default';
|
|
1126
1217
|
return [
|
|
1127
1218
|
`Create Dedicated Runtime "${choices.name}" — it bills while it runs:`,
|
|
1128
1219
|
` size ${choices.sizePreset} — ${spec}`,
|
|
@@ -39,6 +39,18 @@ export declare const labelScopeFlags: {
|
|
|
39
39
|
scope: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
40
40
|
'scope-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* `--allow-duplicate-name`, shared by every command that can put a live name
|
|
44
|
+
* into a scope (create, rename, restore).
|
|
45
|
+
*
|
|
46
|
+
* The SERVER owns the rule (`LabelService.assertNameFreeInOtherScope`): a name
|
|
47
|
+
* another label you can use already carries is refused with
|
|
48
|
+
* `409 duplicate_name_other_scope`, and the refusal names that label. The CLI
|
|
49
|
+
* only relays the override, so every surface — web, MCP, CLI — gets one answer.
|
|
50
|
+
*/
|
|
51
|
+
export declare const allowDuplicateNameFlags: {
|
|
52
|
+
'allow-duplicate-name': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
53
|
+
};
|
|
42
54
|
export interface ResolvedLabelScope {
|
|
43
55
|
scope: LabelScope;
|
|
44
56
|
scopeId?: string;
|