@neta-art/cohub-cli 8.4.0 → 9.0.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/README.md +25 -2
- package/dist/avatar.js +3 -1
- package/dist/board-export.d.ts +22 -41
- package/dist/board-export.js +178 -115
- package/dist/board-fonts.d.ts +7 -0
- package/dist/board-fonts.js +10 -0
- package/dist/board-kernel/browserAll.js +2760 -0
- package/dist/board-kernel/getPo2TextureFromSource.js +13138 -0
- package/dist/board-kernel/init.js +867 -0
- package/dist/board-kernel/webworkerAll.js +3 -0
- package/dist/board-kernel.d.ts +2 -0
- package/dist/board-kernel.js +28812 -0
- package/dist/board-time.d.ts +3 -0
- package/dist/board-time.js +28 -0
- package/dist/commands/apps.js +2 -2
- package/dist/commands/auth.js +2 -2
- package/dist/commands/boards/examples.d.ts +8 -2
- package/dist/commands/boards/examples.js +79 -411
- package/dist/commands/boards.d.ts +4 -9
- package/dist/commands/boards.js +356 -286
- package/dist/commands/desktop.d.ts +1 -5
- package/dist/commands/desktop.js +10 -19
- package/dist/commands/generations.js +1 -1
- package/dist/commands/me.js +1 -1
- package/dist/commands/public.js +3 -3
- package/dist/commands/references.js +3 -0
- package/dist/commands/referrals.js +2 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/runtime.js +12 -8
- package/dist/commands/search.js +15 -7
- package/dist/commands/space-activity.js +3 -5
- package/dist/commands/space-displays.d.ts +2 -0
- package/dist/commands/space-displays.js +347 -0
- package/dist/commands/space-files.d.ts +5 -0
- package/dist/commands/space-files.js +215 -0
- package/dist/commands/space-invitations.js +3 -3
- package/dist/commands/spaces.d.ts +3 -11
- package/dist/commands/spaces.js +146 -75
- package/dist/fonts/geist-latin-400-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
- package/dist/index.js +7 -4
- package/dist/runtime/launch.d.ts +2 -0
- package/dist/runtime/launch.js +35 -8
- package/dist/runtime/presentation.d.ts +1 -0
- package/dist/runtime/presentation.js +3 -2
- package/dist/runtime/supervisor.d.ts +1 -0
- package/dist/runtime/supervisor.js +2 -1
- package/dist/self-update.d.ts +1 -0
- package/dist/self-update.js +52 -16
- package/dist/space-files/limit.d.ts +2 -0
- package/dist/space-files/limit.js +16 -0
- package/dist/space-files/operands.d.ts +16 -0
- package/dist/space-files/operands.js +33 -0
- package/dist/space-files/transfer.d.ts +53 -0
- package/dist/space-files/transfer.js +296 -0
- package/dist/space-files/upload.d.ts +14 -0
- package/dist/space-files/upload.js +53 -0
- package/dist/space.d.ts +18 -16
- package/dist/space.js +86 -39
- package/dist/web.d.ts +1 -0
- package/dist/web.js +6 -0
- package/package.json +8 -4
- package/dist/commands/board-domain.d.ts +0 -2
- package/dist/commands/board-domain.js +0 -16
- package/dist/commands/boards/animation.d.ts +0 -2
- package/dist/commands/boards/animation.js +0 -171
- package/dist/commands/boards/appearance.d.ts +0 -2
- package/dist/commands/boards/appearance.js +0 -127
- package/dist/commands/boards/batch.d.ts +0 -4
- package/dist/commands/boards/batch.js +0 -62
- package/dist/commands/boards/connections.d.ts +0 -2
- package/dist/commands/boards/connections.js +0 -95
- package/dist/commands/boards/context.d.ts +0 -31
- package/dist/commands/boards/context.js +0 -71
- package/dist/commands/boards/items.d.ts +0 -2
- package/dist/commands/boards/items.js +0 -184
- package/dist/commands/boards/transactions.d.ts +0 -12
- package/dist/commands/boards/transactions.js +0 -69
package/dist/self-update.js
CHANGED
|
@@ -38,9 +38,9 @@ const readState = () => {
|
|
|
38
38
|
return {};
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
-
const writeState = () => {
|
|
41
|
+
const writeState = (state) => {
|
|
42
42
|
mkdirSync(dirname(STATE_PATH), { recursive: true });
|
|
43
|
-
writeFileSync(STATE_PATH, `${JSON.stringify({
|
|
43
|
+
writeFileSync(STATE_PATH, `${JSON.stringify({ checkedAt: new Date().toISOString(), ...state }, null, 2)}\n`);
|
|
44
44
|
};
|
|
45
45
|
const readInstalledVersion = () => {
|
|
46
46
|
try {
|
|
@@ -51,19 +51,35 @@ const readInstalledVersion = () => {
|
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
+
const parseVersion = (value) => {
|
|
55
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(value.trim());
|
|
56
|
+
if (!match)
|
|
57
|
+
return null;
|
|
58
|
+
const core = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
59
|
+
return { core, prerelease: Boolean(match[4]) };
|
|
60
|
+
};
|
|
61
|
+
export function isNewerVersion(candidate, installed) {
|
|
62
|
+
const next = parseVersion(candidate);
|
|
63
|
+
if (!next)
|
|
64
|
+
return false;
|
|
65
|
+
const current = installed === undefined ? null : parseVersion(installed);
|
|
66
|
+
if (!current)
|
|
67
|
+
return true;
|
|
68
|
+
const [major, minor, patch] = next.core;
|
|
69
|
+
const [installedMajor, installedMinor, installedPatch] = current.core;
|
|
70
|
+
const delta = major - installedMajor || minor - installedMinor || patch - installedPatch;
|
|
71
|
+
return delta === 0 ? current.prerelease && !next.prerelease : delta > 0;
|
|
72
|
+
}
|
|
54
73
|
export function resolveSelfUpdateResult(beforeVersion, afterVersion, updatedByPeer) {
|
|
55
74
|
if (beforeVersion !== undefined && beforeVersion === afterVersion)
|
|
56
75
|
return "current";
|
|
57
76
|
return updatedByPeer ? "updated-by-peer" : "updated";
|
|
58
77
|
}
|
|
59
78
|
export const isCliSelfUpdateDue = () => {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
79
|
+
const checkedAt = Date.parse(readState().checkedAt ?? "");
|
|
80
|
+
if (!Number.isFinite(checkedAt))
|
|
62
81
|
return true;
|
|
63
|
-
|
|
64
|
-
if (!Number.isFinite(lastUpdatedAt))
|
|
65
|
-
return true;
|
|
66
|
-
return Date.now() - lastUpdatedAt >= getIntervalMs();
|
|
82
|
+
return Date.now() - checkedAt >= getIntervalMs();
|
|
67
83
|
};
|
|
68
84
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
85
|
const acquireLock = async (timeoutMs) => {
|
|
@@ -96,9 +112,9 @@ const acquireLock = async (timeoutMs) => {
|
|
|
96
112
|
const releaseLock = () => {
|
|
97
113
|
rmSync(LOCK_PATH, { recursive: true, force: true });
|
|
98
114
|
};
|
|
99
|
-
const
|
|
115
|
+
const runNpm = (args, timeoutMs) => {
|
|
100
116
|
return new Promise((resolve, reject) => {
|
|
101
|
-
const child = spawn("npm",
|
|
117
|
+
const child = spawn("npm", args, {
|
|
102
118
|
stdio: ["ignore", "pipe", "pipe"],
|
|
103
119
|
env: process.env,
|
|
104
120
|
});
|
|
@@ -117,14 +133,14 @@ const runNpmUpdate = (timeoutMs) => {
|
|
|
117
133
|
});
|
|
118
134
|
child.on("close", (code, signal) => {
|
|
119
135
|
clearTimeout(timer);
|
|
136
|
+
const stdout = Buffer.concat(stdoutChunks).toString().trim();
|
|
120
137
|
if (code === 0) {
|
|
121
|
-
resolve();
|
|
138
|
+
resolve(stdout);
|
|
122
139
|
return;
|
|
123
140
|
}
|
|
124
|
-
const stdout = Buffer.concat(stdoutChunks).toString().trim();
|
|
125
141
|
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
126
142
|
const detail = [stderr, stdout].filter(Boolean).join("\n");
|
|
127
|
-
reject(new Error(`npm
|
|
143
|
+
reject(new Error(`npm ${args.join(" ")} failed${signal ? ` (${signal})` : code !== null ? ` (exit ${code})` : ""}${detail ? `:\n${detail}` : ""}`));
|
|
128
144
|
});
|
|
129
145
|
});
|
|
130
146
|
};
|
|
@@ -156,9 +172,29 @@ export async function ensureCliSelfUpdated() {
|
|
|
156
172
|
if (!isCliSelfUpdateDue()) {
|
|
157
173
|
return resolveSelfUpdateResult(beforeVersion, readInstalledVersion(), true);
|
|
158
174
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
// Reinstalling briefly removes the `cohub` bin, so only do it for a newer release.
|
|
176
|
+
let latestVersion;
|
|
177
|
+
try {
|
|
178
|
+
latestVersion = await runNpm(["view", `${PACKAGE_NAME}@latest`, "version"], timeoutMs);
|
|
179
|
+
if (!isNewerVersion(latestVersion, beforeVersion)) {
|
|
180
|
+
writeState({ installedVersion: beforeVersion, latestVersion, result: "current" });
|
|
181
|
+
return "current";
|
|
182
|
+
}
|
|
183
|
+
await runNpm(["install", "-g", `${PACKAGE_NAME}@${latestVersion}`, "--silent"], timeoutMs);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
writeState({
|
|
187
|
+
installedVersion: beforeVersion,
|
|
188
|
+
latestVersion,
|
|
189
|
+
result: "failed",
|
|
190
|
+
error: (error instanceof Error ? error.message : String(error)).slice(0, 500),
|
|
191
|
+
});
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
const afterVersion = readInstalledVersion();
|
|
195
|
+
const result = resolveSelfUpdateResult(beforeVersion, afterVersion, false);
|
|
196
|
+
writeState({ installedVersion: afterVersion, latestVersion, result });
|
|
197
|
+
return result;
|
|
162
198
|
}
|
|
163
199
|
finally {
|
|
164
200
|
releaseLock();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createLimiter(concurrency) {
|
|
2
|
+
let active = 0;
|
|
3
|
+
const waiting = [];
|
|
4
|
+
return async (task) => {
|
|
5
|
+
if (active >= concurrency)
|
|
6
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
7
|
+
active += 1;
|
|
8
|
+
try {
|
|
9
|
+
return await task();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
active -= 1;
|
|
13
|
+
waiting.shift()?.();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SpaceOperand = {
|
|
2
|
+
kind: "space";
|
|
3
|
+
space: string | null;
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export type LocalOperand = {
|
|
7
|
+
kind: "local";
|
|
8
|
+
path: string;
|
|
9
|
+
};
|
|
10
|
+
export type FileOperand = SpaceOperand | LocalOperand;
|
|
11
|
+
export declare class InvalidFileOperandError extends Error {
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function normalizeSpacePath(raw: string): string;
|
|
15
|
+
export declare function parseSpaceFileOperand(raw: string): SpaceOperand;
|
|
16
|
+
export declare function parseCopyOperand(raw: string, currentSpaceDeclared: boolean): FileOperand;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { posix, resolve } from "node:path";
|
|
2
|
+
import { parseSpaceRef } from "@neta-art/cohub";
|
|
3
|
+
export class InvalidFileOperandError extends Error {
|
|
4
|
+
name = "InvalidFileOperandError";
|
|
5
|
+
}
|
|
6
|
+
const WORKSPACE_ROOT = "/workspace";
|
|
7
|
+
const isWorkspacePath = (path) => path === WORKSPACE_ROOT || path.startsWith(`${WORKSPACE_ROOT}/`);
|
|
8
|
+
export function normalizeSpacePath(raw) {
|
|
9
|
+
let path = raw.trim();
|
|
10
|
+
if (isWorkspacePath(path))
|
|
11
|
+
path = path.slice(WORKSPACE_ROOT.length + 1);
|
|
12
|
+
else if (path.startsWith("/"))
|
|
13
|
+
throw new InvalidFileOperandError(`'${raw}' is not a Space path`);
|
|
14
|
+
const normalized = posix.normalize(path || ".").replace(/\/+$/, "");
|
|
15
|
+
return normalized === "." ? "" : normalized;
|
|
16
|
+
}
|
|
17
|
+
function parseSpacePrefix(raw) {
|
|
18
|
+
const colon = raw.indexOf(":");
|
|
19
|
+
if (colon <= 0)
|
|
20
|
+
return null;
|
|
21
|
+
const space = raw.slice(0, colon);
|
|
22
|
+
return parseSpaceRef(space) ? { kind: "space", space, path: normalizeSpacePath(raw.slice(colon + 1)) } : null;
|
|
23
|
+
}
|
|
24
|
+
export function parseSpaceFileOperand(raw) {
|
|
25
|
+
return parseSpacePrefix(raw) ?? { kind: "space", space: null, path: normalizeSpacePath(raw) };
|
|
26
|
+
}
|
|
27
|
+
export function parseCopyOperand(raw, currentSpaceDeclared) {
|
|
28
|
+
const space = parseSpacePrefix(raw);
|
|
29
|
+
if (space)
|
|
30
|
+
return space;
|
|
31
|
+
const local = !currentSpaceDeclared || (raw.startsWith("/") && !isWorkspacePath(raw));
|
|
32
|
+
return local ? { kind: "local", path: resolve(raw) } : { kind: "space", space: null, path: normalizeSpacePath(raw) };
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type SpaceFsCopyResult, type SpaceFsCopyStats, type SpaceFsTreeResponse } from "@neta-art/cohub";
|
|
2
|
+
import { type SpaceFiles } from "./upload.js";
|
|
3
|
+
export type CopyOptions = {
|
|
4
|
+
recursive: boolean;
|
|
5
|
+
noClobber: boolean;
|
|
6
|
+
preserveTimestamps: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare class CopyAbortError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const emptyCopyResult: () => SpaceFsCopyResult;
|
|
12
|
+
export declare function mergeCopyResults(results: SpaceFsCopyResult[]): SpaceFsCopyResult;
|
|
13
|
+
type SpaceNode = {
|
|
14
|
+
type: "dir";
|
|
15
|
+
tree: SpaceFsTreeResponse;
|
|
16
|
+
} | {
|
|
17
|
+
type: "file";
|
|
18
|
+
} | null;
|
|
19
|
+
export declare function statSpacePath(files: SpaceFiles, path: string): Promise<SpaceNode>;
|
|
20
|
+
export declare function checkDestination(input: {
|
|
21
|
+
sourceCount: number;
|
|
22
|
+
display: string;
|
|
23
|
+
stat: (path: string) => Promise<"dir" | "file" | null>;
|
|
24
|
+
path: string;
|
|
25
|
+
parent: string;
|
|
26
|
+
}): Promise<boolean>;
|
|
27
|
+
export declare const spaceNodeType: (files: SpaceFiles, path: string) => Promise<"dir" | "file" | null>;
|
|
28
|
+
export declare const localNodeType: (path: string) => Promise<"dir" | "file" | null>;
|
|
29
|
+
export declare function uploadToSpace(input: {
|
|
30
|
+
files: SpaceFiles;
|
|
31
|
+
sources: string[];
|
|
32
|
+
destination: string;
|
|
33
|
+
intoDirectory: boolean;
|
|
34
|
+
options: CopyOptions;
|
|
35
|
+
onProgress: (stats: SpaceFsCopyStats) => void;
|
|
36
|
+
}): Promise<SpaceFsCopyResult>;
|
|
37
|
+
export declare function downloadFromSpace(input: {
|
|
38
|
+
sources: Array<{
|
|
39
|
+
files: SpaceFiles;
|
|
40
|
+
path: string;
|
|
41
|
+
}>;
|
|
42
|
+
destination: string;
|
|
43
|
+
intoDirectory: boolean;
|
|
44
|
+
options: CopyOptions;
|
|
45
|
+
onProgress: (stats: SpaceFsCopyStats) => void;
|
|
46
|
+
}): Promise<SpaceFsCopyResult>;
|
|
47
|
+
export declare function copyLocal(input: {
|
|
48
|
+
sources: string[];
|
|
49
|
+
destination: string;
|
|
50
|
+
intoDirectory: boolean;
|
|
51
|
+
options: CopyOptions;
|
|
52
|
+
}): Promise<SpaceFsCopyResult>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { cp, lstat, mkdir, readdir, rename, rm, stat, utimes } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, posix } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { UPLOAD_MAX_FILE_BYTES } from "@neta-art/cohub";
|
|
8
|
+
import { createLimiter } from "./limit.js";
|
|
9
|
+
import { uploadLocalFiles } from "./upload.js";
|
|
10
|
+
export class CopyAbortError extends Error {
|
|
11
|
+
name = "CopyAbortError";
|
|
12
|
+
}
|
|
13
|
+
export const emptyCopyResult = () => ({
|
|
14
|
+
files: 0, dirs: 0, symlinks: 0, bytes: 0, paths: [], overwritten: 0, skipped: 0, errors: [],
|
|
15
|
+
});
|
|
16
|
+
export function mergeCopyResults(results) {
|
|
17
|
+
const merged = emptyCopyResult();
|
|
18
|
+
for (const result of results) {
|
|
19
|
+
for (const key of ["files", "dirs", "symlinks", "bytes", "overwritten", "skipped"])
|
|
20
|
+
merged[key] += result[key];
|
|
21
|
+
merged.paths.push(...result.paths);
|
|
22
|
+
merged.errors.push(...result.errors);
|
|
23
|
+
}
|
|
24
|
+
return merged;
|
|
25
|
+
}
|
|
26
|
+
const display = (path) => path || ".";
|
|
27
|
+
const targetOf = (source, destination, intoDirectory, join) => {
|
|
28
|
+
const name = posix.basename(source);
|
|
29
|
+
return intoDirectory && name ? join(destination, name) : destination;
|
|
30
|
+
};
|
|
31
|
+
const joinSpacePath = (parent, name) => (parent ? `${parent}/${name}` : name);
|
|
32
|
+
const lstatOrNull = (path) => lstat(path).catch(() => null);
|
|
33
|
+
export async function statSpacePath(files, path) {
|
|
34
|
+
try {
|
|
35
|
+
return { type: "dir", tree: await files.list(path) };
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const { status, code } = error;
|
|
39
|
+
if (status === 404)
|
|
40
|
+
return null;
|
|
41
|
+
if (code === "not_a_directory")
|
|
42
|
+
return { type: "file" };
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function checkDestination(input) {
|
|
47
|
+
const node = await input.stat(input.path);
|
|
48
|
+
if (input.sourceCount > 1 && node !== "dir")
|
|
49
|
+
throw new CopyAbortError(`target '${input.display}' is not a directory`);
|
|
50
|
+
if (!node && input.parent !== input.path && (await input.stat(input.parent)) !== "dir") {
|
|
51
|
+
throw new CopyAbortError(`cannot create '${input.display}': No such file or directory`);
|
|
52
|
+
}
|
|
53
|
+
return node === "dir";
|
|
54
|
+
}
|
|
55
|
+
export const spaceNodeType = async (files, path) => (await statSpacePath(files, path))?.type ?? null;
|
|
56
|
+
export const localNodeType = async (path) => {
|
|
57
|
+
const stats = await lstatOrNull(path);
|
|
58
|
+
return stats ? (stats.isDirectory() ? "dir" : "file") : null;
|
|
59
|
+
};
|
|
60
|
+
const isUploadableName = (name) => name.length > 0 && name.length <= 255 && name !== "." && name !== ".." && name.trim() === name &&
|
|
61
|
+
!/[<>:"/\\|?*]/.test(name) && ![...name].some((char) => char.charCodeAt(0) <= 0x1f);
|
|
62
|
+
export async function uploadToSpace(input) {
|
|
63
|
+
const { files, options } = input;
|
|
64
|
+
const result = emptyCopyResult();
|
|
65
|
+
const fail = (path, code, message) => {
|
|
66
|
+
result.errors.push({ path, code, message });
|
|
67
|
+
};
|
|
68
|
+
const entries = [];
|
|
69
|
+
const walk = async (localPath, target, stats) => {
|
|
70
|
+
if (stats.isFile()) {
|
|
71
|
+
entries.push({ localPath, relativePath: target, size: stats.size });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!stats.isDirectory()) {
|
|
75
|
+
result.skipped += 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const names = await readdir(localPath);
|
|
79
|
+
await Promise.all(names.map(async (name) => {
|
|
80
|
+
const child = join(localPath, name);
|
|
81
|
+
const childStats = await stat(child).catch(() => null);
|
|
82
|
+
if (childStats)
|
|
83
|
+
await walk(child, joinSpacePath(target, name), childStats);
|
|
84
|
+
}));
|
|
85
|
+
};
|
|
86
|
+
for (const source of input.sources) {
|
|
87
|
+
const stats = await stat(source).catch(() => null);
|
|
88
|
+
if (!stats) {
|
|
89
|
+
fail(source, "path_not_found", `cannot stat '${source}': No such file or directory`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (stats.isDirectory() && !options.recursive) {
|
|
93
|
+
fail(source, "is_a_directory", `-r not specified; omitting directory '${source}'`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const target = targetOf(source, input.destination, input.intoDirectory, joinSpacePath);
|
|
97
|
+
await walk(source, target, stats);
|
|
98
|
+
result.paths.push(target);
|
|
99
|
+
}
|
|
100
|
+
const parents = [...new Set(entries.map((entry) => posix.dirname(entry.relativePath)).map((dir) => (dir === "." ? "" : dir)))];
|
|
101
|
+
const limit = createLimiter(8);
|
|
102
|
+
const existing = new Map();
|
|
103
|
+
await Promise.all(parents.map((dir) => limit(async () => {
|
|
104
|
+
const node = await statSpacePath(files, dir);
|
|
105
|
+
if (node?.type === "dir")
|
|
106
|
+
for (const entry of node.tree.entries)
|
|
107
|
+
existing.set(entry.path, entry.type);
|
|
108
|
+
})));
|
|
109
|
+
const upload = entries.filter((entry) => {
|
|
110
|
+
const current = existing.get(entry.relativePath);
|
|
111
|
+
if (current === "dir") {
|
|
112
|
+
fail(entry.relativePath, "is_a_directory", `cannot overwrite directory '${entry.relativePath}' with non-directory`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (current && options.noClobber) {
|
|
116
|
+
result.skipped += 1;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (!entry.relativePath.split("/").every(isUploadableName)) {
|
|
120
|
+
fail(entry.relativePath, "name_invalid", `cannot upload '${entry.localPath}': unsupported file name`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
if (entry.size > UPLOAD_MAX_FILE_BYTES) {
|
|
124
|
+
fail(entry.relativePath, "file_too_large", `cannot upload '${entry.localPath}': file exceeds the upload size limit`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (current)
|
|
128
|
+
result.overwritten += 1;
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
const progress = { files: 0, dirs: 0, symlinks: 0, bytes: 0 };
|
|
132
|
+
await uploadLocalFiles(files, upload, {
|
|
133
|
+
onUploaded: (entry) => {
|
|
134
|
+
progress.files += 1;
|
|
135
|
+
progress.bytes += entry.size;
|
|
136
|
+
input.onProgress(progress);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return { ...result, ...progress };
|
|
140
|
+
}
|
|
141
|
+
export async function downloadFromSpace(input) {
|
|
142
|
+
const { options } = input;
|
|
143
|
+
const result = emptyCopyResult();
|
|
144
|
+
const fail = (path, code, message) => {
|
|
145
|
+
result.errors.push({ path, code, message });
|
|
146
|
+
};
|
|
147
|
+
const limit = createLimiter(6);
|
|
148
|
+
const report = () => input.onProgress(result);
|
|
149
|
+
const downloadFile = async (files, path, target, mtimeMs) => {
|
|
150
|
+
const current = await lstatOrNull(target);
|
|
151
|
+
if (current?.isDirectory())
|
|
152
|
+
return fail(target, "is_a_directory", `cannot overwrite directory '${target}' with non-directory`);
|
|
153
|
+
if (current && options.noClobber) {
|
|
154
|
+
result.skipped += 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const temp = join(dirname(target), `.cohub-download.${randomUUID().slice(0, 8)}`);
|
|
158
|
+
try {
|
|
159
|
+
const bytes = await limit(async () => {
|
|
160
|
+
const { response } = await files.open(path);
|
|
161
|
+
let written = 0;
|
|
162
|
+
if (response.body) {
|
|
163
|
+
const body = Readable.fromWeb(response.body);
|
|
164
|
+
body.on("data", (chunk) => {
|
|
165
|
+
written += chunk.length;
|
|
166
|
+
});
|
|
167
|
+
await pipeline(body, createWriteStream(temp, { flags: "wx" }));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
await pipeline(Readable.from([]), createWriteStream(temp, { flags: "wx" }));
|
|
171
|
+
}
|
|
172
|
+
return written;
|
|
173
|
+
});
|
|
174
|
+
if (options.preserveTimestamps && mtimeMs !== undefined)
|
|
175
|
+
await utimes(temp, new Date(), new Date(mtimeMs));
|
|
176
|
+
await rename(temp, target);
|
|
177
|
+
result.files += 1;
|
|
178
|
+
result.bytes += bytes;
|
|
179
|
+
if (current)
|
|
180
|
+
result.overwritten += 1;
|
|
181
|
+
report();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await rm(temp, { force: true });
|
|
185
|
+
fail(target, "download_failed", `cannot download '${display(path)}': ${error.message}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const downloadDir = async (files, tree, target) => {
|
|
189
|
+
const current = await lstatOrNull(target);
|
|
190
|
+
if (current && !current.isDirectory()) {
|
|
191
|
+
return fail(target, "not_a_directory", `cannot overwrite non-directory '${target}' with directory '${display(tree.path)}'`);
|
|
192
|
+
}
|
|
193
|
+
if (!current)
|
|
194
|
+
await mkdir(target);
|
|
195
|
+
result.dirs += 1;
|
|
196
|
+
if (tree.entries.length >= 1000) {
|
|
197
|
+
fail(target, "listing_truncated", `cannot copy all of '${display(tree.path)}': directories with 1000 or more entries are not fully listed`);
|
|
198
|
+
}
|
|
199
|
+
await Promise.all(tree.entries.map(async (entry) => {
|
|
200
|
+
const child = join(target, entry.name);
|
|
201
|
+
if (entry.type === "file")
|
|
202
|
+
return downloadFile(files, entry.path, child, entry.mtimeMs);
|
|
203
|
+
if (entry.type === "symlink") {
|
|
204
|
+
result.skipped += 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const node = await limit(() => statSpacePath(files, entry.path));
|
|
208
|
+
if (node?.type === "dir")
|
|
209
|
+
await downloadDir(files, node.tree, child);
|
|
210
|
+
}));
|
|
211
|
+
};
|
|
212
|
+
for (const { files, path } of input.sources) {
|
|
213
|
+
const node = await statSpacePath(files, path);
|
|
214
|
+
if (!node) {
|
|
215
|
+
fail(path, "path_not_found", `cannot stat '${display(path)}': No such file or directory`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (node.type === "dir" && !options.recursive) {
|
|
219
|
+
fail(path, "is_a_directory", `-r not specified; omitting directory '${display(path)}'`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const target = targetOf(path, input.destination, input.intoDirectory, join);
|
|
223
|
+
if (node.type === "dir") {
|
|
224
|
+
await downloadDir(files, node.tree, target);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const parent = options.preserveTimestamps ? await statSpacePath(files, posix.dirname(path) === "." ? "" : posix.dirname(path)) : null;
|
|
228
|
+
const mtimeMs = parent?.type === "dir" ? parent.tree.entries.find((entry) => entry.path === path)?.mtimeMs : undefined;
|
|
229
|
+
await downloadFile(files, path, target, mtimeMs);
|
|
230
|
+
}
|
|
231
|
+
result.paths.push(target);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
export async function copyLocal(input) {
|
|
236
|
+
const { options } = input;
|
|
237
|
+
const result = emptyCopyResult();
|
|
238
|
+
const fail = (path, code, message) => {
|
|
239
|
+
result.errors.push({ path, code, message });
|
|
240
|
+
};
|
|
241
|
+
for (const source of input.sources) {
|
|
242
|
+
const stats = await lstatOrNull(source);
|
|
243
|
+
if (!stats) {
|
|
244
|
+
fail(source, "path_not_found", `cannot stat '${source}': No such file or directory`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (stats.isDirectory() && !options.recursive) {
|
|
248
|
+
fail(source, "is_a_directory", `-r not specified; omitting directory '${source}'`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const target = targetOf(source, input.destination, input.intoDirectory, join);
|
|
252
|
+
if (target === source) {
|
|
253
|
+
fail(source, "same_file", `'${source}' and '${target}' are the same file`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (stats.isDirectory() && target.startsWith(`${source}/`)) {
|
|
257
|
+
fail(source, "copy_into_itself", `cannot copy a directory, '${source}', into itself, '${target}'`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await cp(source, target, {
|
|
262
|
+
recursive: true,
|
|
263
|
+
force: !options.noClobber,
|
|
264
|
+
errorOnExist: false,
|
|
265
|
+
preserveTimestamps: options.preserveTimestamps,
|
|
266
|
+
verbatimSymlinks: true,
|
|
267
|
+
filter: async (from, to) => {
|
|
268
|
+
const node = await lstat(from);
|
|
269
|
+
if (node.isDirectory()) {
|
|
270
|
+
result.dirs += 1;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const existing = await lstatOrNull(to);
|
|
274
|
+
if (existing && options.noClobber) {
|
|
275
|
+
result.skipped += 1;
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (existing)
|
|
279
|
+
result.overwritten += 1;
|
|
280
|
+
if (node.isSymbolicLink())
|
|
281
|
+
result.symlinks += 1;
|
|
282
|
+
else {
|
|
283
|
+
result.files += 1;
|
|
284
|
+
result.bytes += node.size;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
result.paths.push(target);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
fail(target, "copy_failed", `cannot copy '${source}': ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CohubHttpClient, type SpaceFsUploadEntry } from "@neta-art/cohub";
|
|
2
|
+
export type SpaceFiles = ReturnType<CohubHttpClient["space"]>["files"];
|
|
3
|
+
export type LocalUploadEntry = {
|
|
4
|
+
localPath: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
size: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function uploadLocalFiles(files: SpaceFiles, entries: LocalUploadEntry[], options?: {
|
|
9
|
+
targetDir?: string;
|
|
10
|
+
onUploaded?: (entry: LocalUploadEntry) => void;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
uploaded: SpaceFsUploadEntry[];
|
|
13
|
+
uploadIds: string[];
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, } from "@neta-art/cohub";
|
|
3
|
+
import { putLocalFile } from "../http-put.js";
|
|
4
|
+
import { createLimiter } from "./limit.js";
|
|
5
|
+
const PUT_CONCURRENCY = 4;
|
|
6
|
+
function batches(entries) {
|
|
7
|
+
const result = [];
|
|
8
|
+
let batch = [];
|
|
9
|
+
let bytes = 0;
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (batch.length > 0 && (batch.length >= UPLOAD_MAX_BATCH_FILES || bytes + entry.size > UPLOAD_MAX_BATCH_BYTES)) {
|
|
12
|
+
result.push(batch);
|
|
13
|
+
batch = [];
|
|
14
|
+
bytes = 0;
|
|
15
|
+
}
|
|
16
|
+
batch.push(entry);
|
|
17
|
+
bytes += entry.size;
|
|
18
|
+
}
|
|
19
|
+
if (batch.length > 0)
|
|
20
|
+
result.push(batch);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
export async function uploadLocalFiles(files, entries, options = {}) {
|
|
24
|
+
const uploaded = [];
|
|
25
|
+
const uploadIds = [];
|
|
26
|
+
const limit = createLimiter(PUT_CONCURRENCY);
|
|
27
|
+
for (const batch of batches(entries)) {
|
|
28
|
+
const byId = new Map(batch.map((entry) => [randomUUID(), entry]));
|
|
29
|
+
const plan = await files.createUpload({
|
|
30
|
+
destination: { kind: "workspace", targetDir: options.targetDir },
|
|
31
|
+
entries: [...byId].map(([id, entry]) => ({
|
|
32
|
+
id,
|
|
33
|
+
name: entry.relativePath.split("/").at(-1) ?? entry.relativePath,
|
|
34
|
+
relativePath: entry.relativePath,
|
|
35
|
+
size: entry.size,
|
|
36
|
+
mimeType: null,
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
await Promise.all(plan.entries.map((item) => limit(async () => {
|
|
40
|
+
const entry = byId.get(item.id);
|
|
41
|
+
if (!entry)
|
|
42
|
+
throw new Error(`Missing upload entry: ${item.id}`);
|
|
43
|
+
if (item.uploadUrl) {
|
|
44
|
+
await putLocalFile({ url: item.uploadUrl, filePath: entry.localPath, size: entry.size, headers: item.headers, label: entry.relativePath });
|
|
45
|
+
}
|
|
46
|
+
options.onUploaded?.(entry);
|
|
47
|
+
})));
|
|
48
|
+
const result = await files.completeUpload(plan.uploadId, { entries: plan.entries.map((item) => ({ id: item.id })) });
|
|
49
|
+
uploaded.push(...result.uploaded);
|
|
50
|
+
uploadIds.push(plan.uploadId);
|
|
51
|
+
}
|
|
52
|
+
return { uploaded, uploadIds };
|
|
53
|
+
}
|
package/dist/space.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
export declare class SpaceTargetError extends Error {
|
|
3
|
+
readonly detail?: string | undefined;
|
|
4
|
+
name: string;
|
|
5
|
+
constructor(message: string, detail?: string | undefined);
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
8
|
* Cache key aligned with auth: execution token is exclusive (same as
|
|
4
9
|
* `resolveAccessToken`) and never falls back to a local Logto session.
|
|
@@ -12,29 +17,26 @@ export declare function identityKeyFrom(input: {
|
|
|
12
17
|
}): string | null;
|
|
13
18
|
export declare function currentIdentityKey(): string | null;
|
|
14
19
|
/** Exported for tests; production always uses `CACHE_PATH`. */
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function
|
|
20
|
+
export declare function readHomeSpaceCache(path: string, key: string, now?: number): string | null;
|
|
21
|
+
export declare function clearHomeSpaceCache(): void;
|
|
17
22
|
/** Explicit target from `-s/--space` (any ancestor) or `COHUB_SPACE_ID`, else null. */
|
|
18
23
|
export declare function explicitSpace(program: Command): string | null;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* Network and auth failures propagate so callers can report them faithfully.
|
|
24
|
-
*/
|
|
25
|
-
export declare function resolveDefaultSpace(): Promise<string | null>;
|
|
24
|
+
/** Cached per identity and memoized per process. */
|
|
25
|
+
export declare function resolveHomeSpace(): Promise<string>;
|
|
26
|
+
export declare function resolveSpaceRef(value: string): Promise<string>;
|
|
27
|
+
export declare function failSpaceTarget(e: unknown): never;
|
|
26
28
|
/** Shared exit for commands that need a space but resolved none. */
|
|
27
29
|
export declare function missingSpaceError(): never;
|
|
28
|
-
/**
|
|
29
|
-
* Resolve a Space from an optional explicit target, the current directory
|
|
30
|
-
* binding, and finally the user's Home Space.
|
|
31
|
-
*/
|
|
32
30
|
export declare function resolveBoundSpace(options?: {
|
|
33
31
|
cwd?: string;
|
|
34
32
|
bindingsPath?: string;
|
|
35
33
|
}): Promise<string | null>;
|
|
36
|
-
export
|
|
34
|
+
export type SpaceTargetOptions = {
|
|
35
|
+
/** Only for commands that start new work. */
|
|
36
|
+
home?: boolean;
|
|
37
37
|
cwd?: string;
|
|
38
38
|
bindingsPath?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
39
|
+
};
|
|
40
|
+
/** Explicit target or `COHUB_SPACE_ID`, then the directory binding, then Home when allowed. */
|
|
41
|
+
export declare function resolveSpaceTarget(target?: string | null, options?: SpaceTargetOptions): Promise<string>;
|
|
42
|
+
export declare function resolveSpace(command: Command, options?: Pick<SpaceTargetOptions, "home">): Promise<string>;
|