ninepm 0.49.0 → 0.52.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/dist/index.d.ts +21 -1
- package/dist/index.js +187 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -114,7 +114,7 @@ export interface CreatedServiceToken {
|
|
|
114
114
|
clientSecret: string;
|
|
115
115
|
}
|
|
116
116
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
117
|
-
export declare const CLI_VERSION = "0.
|
|
117
|
+
export declare const CLI_VERSION = "0.52.0";
|
|
118
118
|
type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
|
|
119
119
|
export interface StoredCredentialEntry {
|
|
120
120
|
token: string;
|
|
@@ -284,6 +284,24 @@ export declare function checkCliVersion(options: {
|
|
|
284
284
|
fetchFn?: FetchLike;
|
|
285
285
|
timeoutMs?: number;
|
|
286
286
|
}): Promise<CliVersionCheck>;
|
|
287
|
+
export interface UpdateNudgeOptions {
|
|
288
|
+
fetchFn?: FetchLike;
|
|
289
|
+
now?: () => Date;
|
|
290
|
+
cachePath?: string;
|
|
291
|
+
env?: NodeJS.ProcessEnv;
|
|
292
|
+
}
|
|
293
|
+
export declare function deployUpdateNudge(options?: UpdateNudgeOptions): Promise<string | null>;
|
|
294
|
+
export interface SkillFreshness {
|
|
295
|
+
status: "up_to_date" | "stale" | "not_installed" | "unknown";
|
|
296
|
+
installedStamp?: string;
|
|
297
|
+
publishedStamp?: string;
|
|
298
|
+
detail?: string;
|
|
299
|
+
}
|
|
300
|
+
export declare function checkSkillFreshness(options?: {
|
|
301
|
+
fetchFn?: FetchLike;
|
|
302
|
+
skillPath?: string;
|
|
303
|
+
timeoutMs?: number;
|
|
304
|
+
}): Promise<SkillFreshness>;
|
|
287
305
|
export interface ListSitesResult {
|
|
288
306
|
projects: Project[];
|
|
289
307
|
}
|
|
@@ -353,6 +371,8 @@ export interface DeviceAuthProgressReporter {
|
|
|
353
371
|
stop(): void;
|
|
354
372
|
}
|
|
355
373
|
export declare function createDeviceAuthProgressReporter(stream?: NodeJS.WriteStream): DeviceAuthProgressReporter;
|
|
374
|
+
export declare function noteCliOutdatedAdvisory(response: Response, log?: (line: string) => void): void;
|
|
375
|
+
export declare function resetCliOutdatedAdvisoryForTests(): void;
|
|
356
376
|
type CliCommand = {
|
|
357
377
|
kind: "deploy";
|
|
358
378
|
options: DeployOptions;
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { MAX_DEPLOY_DESCRIPTION_LENGTH, normalizeDeployDescription } from "./dep
|
|
|
14
14
|
import { MAX_STATIC_ASSET_FILE_BYTES, MAX_WORKER_ENTRY_COMPRESSED_BYTES, MAX_WORKER_SCRIPT_COMPRESSED_BYTES, WORKER_ENTRY_COMPRESSED_WARN_BYTES, assetFileTooLargeMessage, formatMebibytes, formatMegabytes, oversizedAssets, workerBundleNearLimitMessage, workerBundleTooLargeMessage, workerEntryNearLimitMessage, workerEntryTooLargeMessage, } from "./deploy-limits.js";
|
|
15
15
|
import { buildFileManifest, readManifestFile } from "./manifest.js";
|
|
16
16
|
import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
|
|
17
|
-
export const CLI_VERSION = "0.
|
|
17
|
+
export const CLI_VERSION = "0.52.0";
|
|
18
18
|
const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
|
|
19
19
|
const TOKEN_SERVICE = "9pm";
|
|
20
20
|
const TOKEN_ACCOUNT = "default";
|
|
@@ -499,6 +499,7 @@ async function runDeploy(initialOptions, slugHolder) {
|
|
|
499
499
|
method: upload.method,
|
|
500
500
|
headers: {
|
|
501
501
|
authorization: `Bearer ${options.token}`,
|
|
502
|
+
...cliVersionHeader(),
|
|
502
503
|
...upload.headers,
|
|
503
504
|
},
|
|
504
505
|
body: arrayBufferFromBytes(body),
|
|
@@ -856,8 +857,9 @@ async function doctor(options) {
|
|
|
856
857
|
catch (error) {
|
|
857
858
|
versionError = error instanceof Error ? error.message : String(error);
|
|
858
859
|
}
|
|
860
|
+
const skill = await checkSkillFreshness();
|
|
859
861
|
if (options.json) {
|
|
860
|
-
console.log(JSON.stringify({ apiUrl: normalizeApiUrl(options.apiUrl), credentials, account, accountError, version: versionCheck, versionError }, null, 2));
|
|
862
|
+
console.log(JSON.stringify({ apiUrl: normalizeApiUrl(options.apiUrl), credentials, account, accountError, version: versionCheck, versionError, skill }, null, 2));
|
|
861
863
|
return;
|
|
862
864
|
}
|
|
863
865
|
console.log("9pm.ai doctor");
|
|
@@ -898,6 +900,18 @@ async function doctor(options) {
|
|
|
898
900
|
else if (versionError) {
|
|
899
901
|
console.log(`CLI: ${CLI_VERSION} (latest unavailable: ${versionError})`);
|
|
900
902
|
}
|
|
903
|
+
if (skill.status === "up_to_date") {
|
|
904
|
+
console.log(`Skill: up to date (stamp ${skill.installedStamp})`);
|
|
905
|
+
}
|
|
906
|
+
else if (skill.status === "stale") {
|
|
907
|
+
console.log(`Skill: stale (installed ${skill.installedStamp ?? "no stamp"}, published ${skill.publishedStamp}). Refresh: ${SKILL_REFRESH_COMMAND}`);
|
|
908
|
+
}
|
|
909
|
+
else if (skill.status === "not_installed") {
|
|
910
|
+
console.log("Skill: not installed (expected at ~/.agents/skills/9pm/SKILL.md)");
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
console.log(`Skill: installed; freshness unknown (${skill.detail ?? "no detail"})`);
|
|
914
|
+
}
|
|
901
915
|
}
|
|
902
916
|
export async function getSiteAccess(options) {
|
|
903
917
|
return apiRequest(options, `/v1/sites/${encodeURIComponent(options.site)}/access`, {
|
|
@@ -1021,6 +1035,122 @@ export async function checkCliVersion(options) {
|
|
|
1021
1035
|
const latest = await fetchLatestCliVersion(options);
|
|
1022
1036
|
return { current: CLI_VERSION, latest, comparison: compareCliVersions(CLI_VERSION, latest.version) };
|
|
1023
1037
|
}
|
|
1038
|
+
// Passive update nudge after a successful deploy (#996). Deliberately quiet:
|
|
1039
|
+
// at most one registry probe per interval (answer cached in the credential
|
|
1040
|
+
// config dir), a short timeout so a slow registry never holds up a deploy that
|
|
1041
|
+
// already finished, and every failure — network, malformed cache, unwritable
|
|
1042
|
+
// config dir — resolves to null, so the nudge can only ever add a line.
|
|
1043
|
+
const UPDATE_NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1044
|
+
const UPDATE_NUDGE_TIMEOUT_MS = 1500;
|
|
1045
|
+
const UPDATE_NUDGE_CACHE_FILE = "update-check.json";
|
|
1046
|
+
const NO_UPDATE_CHECK_ENV = "NINEPM_NO_UPDATE_CHECK";
|
|
1047
|
+
export async function deployUpdateNudge(options = {}) {
|
|
1048
|
+
const env = options.env ?? process.env;
|
|
1049
|
+
if (isTruthyEnv(env[NO_UPDATE_CHECK_ENV])) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
const now = options.now?.() ?? new Date();
|
|
1053
|
+
const cachePath = options.cachePath ?? path.join(credentialConfigDir(), UPDATE_NUDGE_CACHE_FILE);
|
|
1054
|
+
const cache = readUpdateCheckCache(cachePath);
|
|
1055
|
+
// A checkedAt in the future (clock rollback) reads as fresh on purpose —
|
|
1056
|
+
// expiring it would probe the registry on every deploy until the clock
|
|
1057
|
+
// catches back up.
|
|
1058
|
+
const fresh = cache.checkedAtMs !== null && now.getTime() - cache.checkedAtMs < UPDATE_NUDGE_INTERVAL_MS;
|
|
1059
|
+
let latest = cache.latest;
|
|
1060
|
+
if (!fresh) {
|
|
1061
|
+
try {
|
|
1062
|
+
const fetched = await fetchLatestCliVersion({
|
|
1063
|
+
apiUrl: "", // unused: the updater sources the public npm registry (see fetchLatestCliVersion)
|
|
1064
|
+
timeoutMs: UPDATE_NUDGE_TIMEOUT_MS,
|
|
1065
|
+
...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),
|
|
1066
|
+
});
|
|
1067
|
+
latest = fetched.version;
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
// Keep the last-known answer (if any) for the comparison below. The
|
|
1071
|
+
// attempt is still stamped: an unreachable registry must not re-add its
|
|
1072
|
+
// timeout to every deploy for the rest of the interval.
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1076
|
+
writeFileSync(cachePath, JSON.stringify({ checkedAt: now.toISOString(), ...(latest ? { latest } : {}) }));
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
// The cache only spaces out probes; a read-only config dir just means the
|
|
1080
|
+
// next deploy probes again.
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (!latest || compareCliVersions(CLI_VERSION, latest) !== "behind") {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
return `Update available: 9pm CLI ${CLI_VERSION} -> ${latest}. Run: npm i -g ninepm@latest`;
|
|
1087
|
+
}
|
|
1088
|
+
// Skill-freshness half of `9pm doctor` (#996): compare the installed skill's
|
|
1089
|
+
// stamp line against the public source copy — the same reference SKILL.md's
|
|
1090
|
+
// own "Keeping this skill fresh" section names, and the source the install
|
|
1091
|
+
// command actually pulls from (the platform-hosted copy tracks releases and
|
|
1092
|
+
// can lag it). Never throws: doctor is diagnostics, not a gate.
|
|
1093
|
+
const SKILL_INSTALL_PATH_SEGMENTS = [".agents", "skills", "9pm", "SKILL.md"];
|
|
1094
|
+
const SKILL_SOURCE_URL = "https://raw.githubusercontent.com/tmrw-realityos/9pm-skill/main/skills/9pm/SKILL.md";
|
|
1095
|
+
const SKILL_STAMP_PATTERN = /^Skill guide stamp:\s*(\S+)/m;
|
|
1096
|
+
const SKILL_REFRESH_COMMAND = "npx skills add tmrw-realityos/9pm-skill --skill 9pm -g";
|
|
1097
|
+
export async function checkSkillFreshness(options = {}) {
|
|
1098
|
+
const skillPath = options.skillPath ?? path.join(os.homedir(), ...SKILL_INSTALL_PATH_SEGMENTS);
|
|
1099
|
+
let installed;
|
|
1100
|
+
try {
|
|
1101
|
+
installed = readFileSync(skillPath, "utf8");
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
return { status: "not_installed" };
|
|
1105
|
+
}
|
|
1106
|
+
const installedStamp = SKILL_STAMP_PATTERN.exec(installed)?.[1] ?? null;
|
|
1107
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
1108
|
+
const controller = new AbortController();
|
|
1109
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 3000);
|
|
1110
|
+
let publishedStamp = null;
|
|
1111
|
+
try {
|
|
1112
|
+
const response = await fetchFn(SKILL_SOURCE_URL, { signal: controller.signal });
|
|
1113
|
+
if (!response.ok) {
|
|
1114
|
+
throw new Error(`skill source returned ${response.status}`);
|
|
1115
|
+
}
|
|
1116
|
+
publishedStamp = SKILL_STAMP_PATTERN.exec(await response.text())?.[1] ?? null;
|
|
1117
|
+
}
|
|
1118
|
+
catch (error) {
|
|
1119
|
+
return {
|
|
1120
|
+
status: "unknown",
|
|
1121
|
+
...(installedStamp ? { installedStamp } : {}),
|
|
1122
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
clearTimeout(timer);
|
|
1127
|
+
}
|
|
1128
|
+
if (!publishedStamp) {
|
|
1129
|
+
return { status: "unknown", ...(installedStamp ? { installedStamp } : {}), detail: "published copy has no stamp line" };
|
|
1130
|
+
}
|
|
1131
|
+
// An installed copy with no stamp predates the stamp convention, which is
|
|
1132
|
+
// itself proof it is behind.
|
|
1133
|
+
if (!installedStamp || installedStamp !== publishedStamp) {
|
|
1134
|
+
return { status: "stale", ...(installedStamp ? { installedStamp } : {}), publishedStamp };
|
|
1135
|
+
}
|
|
1136
|
+
return { status: "up_to_date", installedStamp, publishedStamp };
|
|
1137
|
+
}
|
|
1138
|
+
// Best-effort read of the update-check cache; the caller judges freshness.
|
|
1139
|
+
// checkedAtMs is null when the file is missing or malformed (so the next probe
|
|
1140
|
+
// runs now), and latest is null when the last attempt failed to get an answer
|
|
1141
|
+
// — a failed attempt is cached too, so the outage's timeout is paid at most
|
|
1142
|
+
// once per interval rather than on every deploy.
|
|
1143
|
+
function readUpdateCheckCache(cachePath) {
|
|
1144
|
+
try {
|
|
1145
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
1146
|
+
const checkedAtMs = typeof parsed.checkedAt === "string" ? Date.parse(parsed.checkedAt) : Number.NaN;
|
|
1147
|
+
const latest = typeof parsed.latest === "string" && parsed.latest.trim() ? parsed.latest.trim() : null;
|
|
1148
|
+
return { checkedAtMs: Number.isFinite(checkedAtMs) ? checkedAtMs : null, latest };
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return { checkedAtMs: null, latest: null };
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1024
1154
|
export async function listSites(options) {
|
|
1025
1155
|
const payload = await apiRequest(options, "/v1/sites", { method: "GET" });
|
|
1026
1156
|
return { projects: payload.projects };
|
|
@@ -1048,7 +1178,7 @@ async function deviceAuthFlow(options) {
|
|
|
1048
1178
|
// made here; an unpredictable store failure is caught after the poll instead.
|
|
1049
1179
|
const startResponse = await fetchFn(`${apiUrl}/v1/oauth/device/start`, {
|
|
1050
1180
|
method: "POST",
|
|
1051
|
-
headers: { "content-type": "application/json" },
|
|
1181
|
+
headers: { "content-type": "application/json", ...cliVersionHeader() },
|
|
1052
1182
|
body: JSON.stringify({ machineName: options.machineName ?? os.hostname(), purpose: options.purpose }),
|
|
1053
1183
|
});
|
|
1054
1184
|
// The control plane refuses a signup it knows can never be approved (#507) with
|
|
@@ -1114,7 +1244,7 @@ async function deviceAuthFlow(options) {
|
|
|
1114
1244
|
await sleep(intervalMs);
|
|
1115
1245
|
const pollResponse = await fetchFn(`${apiUrl}/v1/oauth/device/poll`, {
|
|
1116
1246
|
method: "POST",
|
|
1117
|
-
headers: { "content-type": "application/json" },
|
|
1247
|
+
headers: { "content-type": "application/json", ...cliVersionHeader() },
|
|
1118
1248
|
body: JSON.stringify({ code: deviceCode }),
|
|
1119
1249
|
});
|
|
1120
1250
|
if (pollResponse.status === 400) {
|
|
@@ -1283,6 +1413,7 @@ export async function putManagedFile(options) {
|
|
|
1283
1413
|
headers: {
|
|
1284
1414
|
authorization: `Bearer ${options.token}`,
|
|
1285
1415
|
"content-type": "application/octet-stream",
|
|
1416
|
+
...cliVersionHeader(),
|
|
1286
1417
|
},
|
|
1287
1418
|
body: bytes,
|
|
1288
1419
|
});
|
|
@@ -1293,7 +1424,7 @@ export async function getManagedFile(options) {
|
|
|
1293
1424
|
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
1294
1425
|
const response = await fetchFn(`${apiUrl}${filesRoute(options)}/${encodeURIComponent(options.fileId)}`, {
|
|
1295
1426
|
method: "GET",
|
|
1296
|
-
headers: { authorization: `Bearer ${options.token}
|
|
1427
|
+
headers: { authorization: `Bearer ${options.token}`, ...cliVersionHeader() },
|
|
1297
1428
|
});
|
|
1298
1429
|
await expectOk(response, "download file");
|
|
1299
1430
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
@@ -1366,7 +1497,7 @@ async function finalizeDeployment(fetchFn, apiUrl, token, deploymentId) {
|
|
|
1366
1497
|
async function getDeployment(fetchFn, apiUrl, token, deploymentId) {
|
|
1367
1498
|
const response = await fetchFn(`${apiUrl}/v1/deployments/${encodeURIComponent(deploymentId)}`, {
|
|
1368
1499
|
method: "GET",
|
|
1369
|
-
headers: { authorization: `Bearer ${token}
|
|
1500
|
+
headers: { authorization: `Bearer ${token}`, ...cliVersionHeader() },
|
|
1370
1501
|
});
|
|
1371
1502
|
const payload = await expectJson(response, "get deployment");
|
|
1372
1503
|
return payload.deployment;
|
|
@@ -1803,20 +1934,56 @@ function jsonHeaders(token) {
|
|
|
1803
1934
|
return {
|
|
1804
1935
|
authorization: `Bearer ${token}`,
|
|
1805
1936
|
"content-type": "application/json",
|
|
1806
|
-
|
|
1937
|
+
...cliVersionHeader(),
|
|
1807
1938
|
};
|
|
1808
1939
|
}
|
|
1940
|
+
// Every control-plane request must carry the CLI's version (#996): the server's
|
|
1941
|
+
// version floor only judges requests that have it, so a hand-built header object
|
|
1942
|
+
// that omits it silently exempts that call from both the advisory and an
|
|
1943
|
+
// enforced floor. Spread this into any headers literal that bypasses
|
|
1944
|
+
// apiRequest/jsonHeaders.
|
|
1945
|
+
function cliVersionHeader() {
|
|
1946
|
+
return { "x-9pm-cli-version": CLI_VERSION };
|
|
1947
|
+
}
|
|
1809
1948
|
async function expectJson(response, action) {
|
|
1810
1949
|
await expectOk(response, action);
|
|
1811
1950
|
return (await response.json());
|
|
1812
1951
|
}
|
|
1813
1952
|
async function expectOk(response, action) {
|
|
1953
|
+
noteCliOutdatedAdvisory(response);
|
|
1814
1954
|
if (response.ok) {
|
|
1815
1955
|
return;
|
|
1816
1956
|
}
|
|
1957
|
+
// 426 is the control plane's enforced CLI floor (#996): the server-written
|
|
1958
|
+
// message IS the response — surface it alone rather than burying it in the
|
|
1959
|
+
// raw envelope the generic path prints.
|
|
1960
|
+
if (response.status === 426) {
|
|
1961
|
+
throw new Error(await apiErrorMessage(response, action));
|
|
1962
|
+
}
|
|
1817
1963
|
const text = await response.text();
|
|
1818
1964
|
throw new Error(`Failed to ${action}: ${response.status} ${text}`);
|
|
1819
1965
|
}
|
|
1966
|
+
// Advisory half of the control plane's CLI floor (#996): when a response says
|
|
1967
|
+
// this version is below the platform's recommended minimum, say so once per
|
|
1968
|
+
// invocation, on stderr so command output (including --json) stays parseable.
|
|
1969
|
+
const CLI_OUTDATED_HEADER = "x-9pm-cli-outdated";
|
|
1970
|
+
let cliOutdatedAdvisoryPrinted = false;
|
|
1971
|
+
export function noteCliOutdatedAdvisory(response, log = (line) => console.error(line)) {
|
|
1972
|
+
if (cliOutdatedAdvisoryPrinted) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const min = response.headers.get(CLI_OUTDATED_HEADER);
|
|
1976
|
+
if (!min) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
cliOutdatedAdvisoryPrinted = true;
|
|
1980
|
+
log(`Note: this CLI (${CLI_VERSION}) is older than the platform's recommended minimum (${min}). Update: npm i -g ninepm@latest`);
|
|
1981
|
+
}
|
|
1982
|
+
// Test seam: the once-per-invocation latch above is module state, which one
|
|
1983
|
+
// test process shares across cases.
|
|
1984
|
+
export function resetCliOutdatedAdvisoryForTests() {
|
|
1985
|
+
cliOutdatedAdvisoryPrinted = false;
|
|
1986
|
+
}
|
|
1820
1987
|
// Pull the control plane's own message out of its {error:{code,message}} envelope,
|
|
1821
1988
|
// for the few refusals whose message is written for the tester and is the whole
|
|
1822
1989
|
// point of the response (e.g. device signup being unavailable, #507). Dumping the
|
|
@@ -3686,6 +3853,14 @@ async function main() {
|
|
|
3686
3853
|
console.log(`Status: ${result.deployment.status}. Run 9pm apps to check when it becomes active.`);
|
|
3687
3854
|
}
|
|
3688
3855
|
console.log(`Manage: ${PUBLIC_DASHBOARD_URL}`);
|
|
3856
|
+
// Gated the same way as the reachability probe above: a programmatic
|
|
3857
|
+
// caller injecting fetchFn opted out of the CLI's own network side trips.
|
|
3858
|
+
if (!command.options.fetchFn) {
|
|
3859
|
+
const nudge = await deployUpdateNudge();
|
|
3860
|
+
if (nudge) {
|
|
3861
|
+
console.log(nudge);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3689
3864
|
return;
|
|
3690
3865
|
}
|
|
3691
3866
|
if (command.kind === "login") {
|
|
@@ -4161,7 +4336,11 @@ Container deploys:
|
|
|
4161
4336
|
--cpus <n> and --memory <size> (512m, 2048, 4g); the platform picks a placement that fits.
|
|
4162
4337
|
All of these may require platform enablement.
|
|
4163
4338
|
A redeploy KEEPS the app's existing disk automatically: omit --volume and the current disk is
|
|
4164
|
-
carried forward.
|
|
4339
|
+
carried forward. A LARGER --volume than the current disk grows it in place (same disk, data
|
|
4340
|
+
preserved). Sizes never shrink: a smaller --volume is refused — to recreate the disk smaller,
|
|
4341
|
+
drop it first (--no-volume --allow-data-wipe, which destroys its data), then deploy again
|
|
4342
|
+
with the smaller --volume (fresh empty disk).
|
|
4343
|
+
To remove the disk on purpose pass --no-volume (and --allow-data-wipe, since it
|
|
4165
4344
|
destroys the data). If a disk was previously detached by an old CLI, redeploy with --volume <same
|
|
4166
4345
|
gb> --region <same region> to reattach the existing disk (it is reused, not recreated empty).
|
|
4167
4346
|
--allow-data-wipe acknowledges that dropping a disk, or switching to a placement that can't keep
|