cloudtifact 0.2.3 → 0.2.4
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.js +733 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs, { createWriteStream, existsSync, lstatSync, realpathSync, statSync, watch } from "node:fs";
|
|
2
|
+
import fs, { createWriteStream, existsSync, lstatSync, readFileSync, realpathSync, statSync, watch } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -6013,6 +6013,18 @@ const auditActionSchema = _enum([
|
|
|
6013
6013
|
"space.updated",
|
|
6014
6014
|
"space.moved",
|
|
6015
6015
|
"space.deleted",
|
|
6016
|
+
"space.archived",
|
|
6017
|
+
"space.unarchived",
|
|
6018
|
+
"space.trashed",
|
|
6019
|
+
"space.restored",
|
|
6020
|
+
"page.added",
|
|
6021
|
+
"space.filed",
|
|
6022
|
+
"folder.created",
|
|
6023
|
+
"folder.renamed",
|
|
6024
|
+
"folder.moved",
|
|
6025
|
+
"folder.deleted",
|
|
6026
|
+
"folder.restored",
|
|
6027
|
+
"space.merged",
|
|
6016
6028
|
"space.link_changed",
|
|
6017
6029
|
"space.person_added",
|
|
6018
6030
|
"space.person_changed",
|
|
@@ -6045,6 +6057,132 @@ const listAuditOutput = object({
|
|
|
6045
6057
|
next: number$1().nullable(),
|
|
6046
6058
|
nextId: string().nullable().optional()
|
|
6047
6059
|
});
|
|
6060
|
+
/**
|
|
6061
|
+
* GET /v1/live-spaces, read only for what the CLI shows (Shared with me): the spaces you can open, in your workspaces
|
|
6062
|
+
* and the ones people added you to elsewhere. The full shape is in cloud-contract.
|
|
6063
|
+
*/
|
|
6064
|
+
const listOpenableSpacesOutput = object({ liveSpaces: array(object({
|
|
6065
|
+
id: string(),
|
|
6066
|
+
name: string(),
|
|
6067
|
+
workspace: workspaceSchema,
|
|
6068
|
+
url: string(),
|
|
6069
|
+
createdBy: string().nullable().optional(),
|
|
6070
|
+
lastActivityAt: number$1().nullable()
|
|
6071
|
+
})) });
|
|
6072
|
+
const activityActionSchema = _enum([
|
|
6073
|
+
"space.created",
|
|
6074
|
+
"space.archived",
|
|
6075
|
+
"space.unarchived",
|
|
6076
|
+
"space.trashed",
|
|
6077
|
+
"space.restored",
|
|
6078
|
+
"page.added",
|
|
6079
|
+
"space.filed",
|
|
6080
|
+
"folder.created",
|
|
6081
|
+
"folder.renamed",
|
|
6082
|
+
"folder.moved",
|
|
6083
|
+
"folder.deleted",
|
|
6084
|
+
"folder.restored",
|
|
6085
|
+
"space.merged"
|
|
6086
|
+
]);
|
|
6087
|
+
const activityFolderSchema = object({
|
|
6088
|
+
id: string(),
|
|
6089
|
+
name: string()
|
|
6090
|
+
});
|
|
6091
|
+
const activityEventSchema = object({
|
|
6092
|
+
id: string(),
|
|
6093
|
+
action: activityActionSchema,
|
|
6094
|
+
actor: userSchema.nullable(),
|
|
6095
|
+
via: string().nullable(),
|
|
6096
|
+
/** The space it was done to, name as it reads now. Null only for folder.* events (v=2). */
|
|
6097
|
+
space: object({
|
|
6098
|
+
id: string(),
|
|
6099
|
+
name: string()
|
|
6100
|
+
}).nullable(),
|
|
6101
|
+
/** page.added: the page's folder in the space ("pricing-d/") and its title. */
|
|
6102
|
+
page: object({
|
|
6103
|
+
path: string(),
|
|
6104
|
+
title: string().nullable()
|
|
6105
|
+
}).nullable(),
|
|
6106
|
+
/** folder.*: the folder, name as it reads now (folder.renamed: the name it got). */
|
|
6107
|
+
folder: activityFolderSchema.nullable().optional(),
|
|
6108
|
+
/**
|
|
6109
|
+
* space.filed and folder.moved: where it was and where it went (null: the top level). folder.renamed: the names.
|
|
6110
|
+
* space.merged: the space merged in.
|
|
6111
|
+
*/
|
|
6112
|
+
from: activityFolderSchema.nullable().optional(),
|
|
6113
|
+
to: activityFolderSchema.nullable().optional(),
|
|
6114
|
+
/** Set when this was itself an Undo: the id of the event it undid. */
|
|
6115
|
+
undoes: string().nullable(),
|
|
6116
|
+
/** Whether the caller can undo it now: it's still the way it left things, and they may do the opposite. */
|
|
6117
|
+
canUndo: boolean(),
|
|
6118
|
+
/** Someone already undid it (an event below it in the list says so). */
|
|
6119
|
+
undone: boolean(),
|
|
6120
|
+
createdAt: number$1()
|
|
6121
|
+
});
|
|
6122
|
+
object({
|
|
6123
|
+
/** Only what this person did (their handle). */
|
|
6124
|
+
person: string().max(64).optional(),
|
|
6125
|
+
before: number().int().positive().optional(),
|
|
6126
|
+
beforeId: string().max(64).optional(),
|
|
6127
|
+
limit: number().int().min(1).max(100).default(50),
|
|
6128
|
+
/** 2: include folder events (space.filed, folder.*), whose space may be null. Omitted: only ACTIVITY_V1_ACTIONS. */
|
|
6129
|
+
v: number().int().optional()
|
|
6130
|
+
});
|
|
6131
|
+
const listActivityOutput = object({
|
|
6132
|
+
events: array(activityEventSchema),
|
|
6133
|
+
next: number$1().nullable(),
|
|
6134
|
+
nextId: string().nullable()
|
|
6135
|
+
});
|
|
6136
|
+
const undoActivityOutput = object({ event: activityEventSchema });
|
|
6137
|
+
/** "archived “Q4 board pack”", for "Jack (with Claude) archived “Q4 board pack”" (byPerson). */
|
|
6138
|
+
function describeActivity(e) {
|
|
6139
|
+
const name = `“${e.space?.name ?? "a space"}”`;
|
|
6140
|
+
const folder = `the folder “${e.folder?.name ?? "folder"}”`;
|
|
6141
|
+
const place = (f) => f ? `“${f.name}”` : "the top level";
|
|
6142
|
+
switch (e.action) {
|
|
6143
|
+
case "space.created": return `made ${name}`;
|
|
6144
|
+
case "space.archived": return `archived ${name}`;
|
|
6145
|
+
case "space.unarchived": return `brought ${name} back from the archive`;
|
|
6146
|
+
case "space.trashed": return `moved ${name} to the Trash`;
|
|
6147
|
+
case "space.restored": return `restored ${name} from the Trash`;
|
|
6148
|
+
case "page.added": return e.undoes ? `took the page “${e.page?.title ?? e.page?.path ?? "page"}” out of ${name}` : `added the page “${e.page?.title ?? e.page?.path ?? "page"}” to ${name}`;
|
|
6149
|
+
case "space.filed": return e.to ? `moved ${name} to “${e.to.name}”` : `moved ${name} to the top level`;
|
|
6150
|
+
case "folder.created": return `made ${folder}`;
|
|
6151
|
+
case "folder.renamed": return `renamed the folder “${e.from?.name ?? "folder"}” to “${e.to?.name ?? e.folder?.name ?? "folder"}”`;
|
|
6152
|
+
case "folder.moved": return e.to ? `moved ${folder} into ${place(e.to)}` : `moved ${folder} to the top level`;
|
|
6153
|
+
case "folder.deleted": return `removed ${folder} (what was in it moved up a level)`;
|
|
6154
|
+
case "folder.restored": return `brought back ${folder}`;
|
|
6155
|
+
case "space.merged": return e.undoes ? `took “${e.from?.name ?? "a space"}” back out of ${name}` : `merged “${e.from?.name ?? "a space"}” into ${name} as a page`;
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
/** A folder's name: one line, no "/" (paths like "Product plans/Pricing" are names joined by "/"). */
|
|
6159
|
+
const folderNameSchema = string().trim().min(1).max(80).refine((n) => !n.includes("/"), "a folder name can't have a / in it");
|
|
6160
|
+
const folderDescriptionSchema = string().trim().max(300);
|
|
6161
|
+
const folderSchema = object({
|
|
6162
|
+
id: string(),
|
|
6163
|
+
parentId: string().nullable(),
|
|
6164
|
+
name: string(),
|
|
6165
|
+
/** One line people and agents read before filing something here. */
|
|
6166
|
+
description: string().nullable(),
|
|
6167
|
+
/** Its names from the top, joined by "/": "Product plans/Pricing". */
|
|
6168
|
+
path: string(),
|
|
6169
|
+
/** Spaces directly inside it that aren't archived or in the Trash. */
|
|
6170
|
+
spaces: number$1().int(),
|
|
6171
|
+
createdAt: number$1()
|
|
6172
|
+
});
|
|
6173
|
+
const listFoldersOutput = object({ folders: array(folderSchema) });
|
|
6174
|
+
object({
|
|
6175
|
+
name: folderNameSchema,
|
|
6176
|
+
parentId: string().max(64).nullable().optional(),
|
|
6177
|
+
description: folderDescriptionSchema.nullable().optional()
|
|
6178
|
+
});
|
|
6179
|
+
object({
|
|
6180
|
+
name: folderNameSchema.optional(),
|
|
6181
|
+
description: folderDescriptionSchema.nullable().optional(),
|
|
6182
|
+
/** Move it inside another folder (null: to the top level). A folder can't go inside itself. */
|
|
6183
|
+
parentId: string().max(64).nullable().optional()
|
|
6184
|
+
});
|
|
6185
|
+
object({ folderId: string().max(64).nullable() });
|
|
6048
6186
|
const viewersSchema = object({
|
|
6049
6187
|
files: boolean(),
|
|
6050
6188
|
history: boolean(),
|
|
@@ -6084,12 +6222,55 @@ const spaceSchema = object({
|
|
|
6084
6222
|
createdAt: number$1(),
|
|
6085
6223
|
createdBy: string().optional(),
|
|
6086
6224
|
updatedAt: number$1().nullable().optional(),
|
|
6087
|
-
snapshotUrl: string().nullable().optional()
|
|
6225
|
+
snapshotUrl: string().nullable().optional(),
|
|
6226
|
+
archivedAt: number$1().nullable().optional(),
|
|
6227
|
+
/** The project its latest version belongs to, when the client said (versionSchema.project). */
|
|
6228
|
+
project: string().nullable().optional(),
|
|
6229
|
+
deletedAt: number$1().nullable().optional(),
|
|
6230
|
+
favorite: number$1().int().nullable().optional(),
|
|
6231
|
+
openedAt: number$1().nullable().optional(),
|
|
6232
|
+
/** The folder it's listed in (folderSchema), or null: the workspace's top level. Missing from older servers. */
|
|
6233
|
+
folderId: string().nullable().optional(),
|
|
6234
|
+
/** Tidy up merged it into another space as a page: that space and the page's folder there. Its links open it. */
|
|
6235
|
+
mergedInto: object({
|
|
6236
|
+
id: string(),
|
|
6237
|
+
page: string()
|
|
6238
|
+
}).nullable().optional()
|
|
6239
|
+
});
|
|
6240
|
+
const tidySpaceSchema = object({
|
|
6241
|
+
id: string(),
|
|
6242
|
+
name: string(),
|
|
6243
|
+
/** The latest version (0: nothing published yet) and when anything last changed. */
|
|
6244
|
+
latestVersion: number$1().int(),
|
|
6245
|
+
updatedAt: number$1(),
|
|
6246
|
+
/** Can go into another space as a page: no comments, uploads or data of its own that the page would leave behind. */
|
|
6247
|
+
canMerge: boolean()
|
|
6248
|
+
});
|
|
6249
|
+
const tidyOutput = object({
|
|
6250
|
+
lookAlikes: array(object({
|
|
6251
|
+
/** What they share, as people read it ("Pricing proposal"). */
|
|
6252
|
+
topic: string(),
|
|
6253
|
+
/** The space the others go into: the first one made, so its link (and its comments) stay. */
|
|
6254
|
+
into: tidySpaceSchema,
|
|
6255
|
+
spaces: array(tidySpaceSchema)
|
|
6256
|
+
})),
|
|
6257
|
+
finished: array(object({
|
|
6258
|
+
space: tidySpaceSchema,
|
|
6259
|
+
/** "Nothing new in 45 days", "Empty for 9 days". */
|
|
6260
|
+
why: string()
|
|
6261
|
+
}))
|
|
6262
|
+
});
|
|
6263
|
+
object({ into: string().max(64) });
|
|
6264
|
+
const mergeSpaceOutput = object({
|
|
6265
|
+
space: spaceSchema,
|
|
6266
|
+
page: string()
|
|
6088
6267
|
});
|
|
6089
6268
|
object({
|
|
6090
6269
|
workspace: workspaceSlugSchema,
|
|
6091
6270
|
name: string().min(1).max(100),
|
|
6092
|
-
visibility: visibilitySchema.default("public")
|
|
6271
|
+
visibility: visibilitySchema.default("public"),
|
|
6272
|
+
/** List it in this folder of the workspace (folderSchema.id); omitted: the top level. */
|
|
6273
|
+
folderId: string().max(64).optional()
|
|
6093
6274
|
});
|
|
6094
6275
|
object({
|
|
6095
6276
|
name: string().min(1).max(100).optional(),
|
|
@@ -6178,8 +6359,35 @@ const visitorsOutput = object({
|
|
|
6178
6359
|
referrer: string().nullable()
|
|
6179
6360
|
}))
|
|
6180
6361
|
});
|
|
6181
|
-
|
|
6362
|
+
const spaceStatusSchema = _enum([
|
|
6363
|
+
"active",
|
|
6364
|
+
"archived",
|
|
6365
|
+
"trash"
|
|
6366
|
+
]);
|
|
6367
|
+
object({
|
|
6368
|
+
workspace: workspaceSlugSchema.optional(),
|
|
6369
|
+
status: spaceStatusSchema.default("active")
|
|
6370
|
+
});
|
|
6371
|
+
object({ spaces: array(spaceIdSchema).max(500) });
|
|
6372
|
+
const favoritesOutput = object({ spaces: array(spaceIdSchema) });
|
|
6182
6373
|
const listSpacesOutput = object({ spaces: array(spaceSchema) });
|
|
6374
|
+
object({
|
|
6375
|
+
q: string().trim().min(1).max(200),
|
|
6376
|
+
workspace: workspaceSlugSchema.optional(),
|
|
6377
|
+
limit: number().int().min(1).max(50).default(10)
|
|
6378
|
+
});
|
|
6379
|
+
const findSpacesOutput = object({ spaces: array(spaceSchema.extend({
|
|
6380
|
+
/** Pages (HTML and Markdown files) in its latest version. */
|
|
6381
|
+
pages: number$1().int(),
|
|
6382
|
+
/** 0 to 1. */
|
|
6383
|
+
score: number$1(),
|
|
6384
|
+
/** Close enough to publish into without being told (topicMatch in find.ts). */
|
|
6385
|
+
strong: boolean(),
|
|
6386
|
+
/** The page whose title matched better than the space's name, if any. */
|
|
6387
|
+
page: string().nullable()
|
|
6388
|
+
})) });
|
|
6389
|
+
/** A project's name as a client sends it: the repo folder's name or what the person typed. */
|
|
6390
|
+
const projectNameSchema = string().trim().min(1).max(100);
|
|
6183
6391
|
const MAX_FILE_BYTES = 16777216;
|
|
6184
6392
|
const manifest = array(object({
|
|
6185
6393
|
path: filePathSchema,
|
|
@@ -6191,7 +6399,15 @@ const diffSchema = object({
|
|
|
6191
6399
|
changed: array(string()),
|
|
6192
6400
|
removed: array(string())
|
|
6193
6401
|
});
|
|
6194
|
-
|
|
6402
|
+
/**
|
|
6403
|
+
* A folder inside the space ("pricing-d/") that these files replace, keeping every file outside it: how a page is
|
|
6404
|
+
* added to a space that holds other pages. Every path must start with it. Omitted: the files are the whole space.
|
|
6405
|
+
*/
|
|
6406
|
+
const pushPrefixSchema = string().max(100).regex(/^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*\/$/, "a folder like pricing-d/, lowercase, ending in /");
|
|
6407
|
+
object({
|
|
6408
|
+
files: manifest,
|
|
6409
|
+
prefix: pushPrefixSchema.optional()
|
|
6410
|
+
});
|
|
6195
6411
|
const pushPlanOutput = object({
|
|
6196
6412
|
baseVersion: number$1().int(),
|
|
6197
6413
|
missing: array(sha256Schema),
|
|
@@ -6200,12 +6416,18 @@ const pushPlanOutput = object({
|
|
|
6200
6416
|
object({
|
|
6201
6417
|
files: manifest,
|
|
6202
6418
|
message: string().max(500).optional(),
|
|
6203
|
-
baseVersion: number$1().int().min(0).optional()
|
|
6419
|
+
baseVersion: number$1().int().min(0).optional(),
|
|
6420
|
+
prefix: pushPrefixSchema.optional(),
|
|
6421
|
+
project: projectNameSchema.optional()
|
|
6204
6422
|
});
|
|
6205
6423
|
const versionSchema = object({
|
|
6206
6424
|
number: number$1().int(),
|
|
6207
6425
|
message: string().nullable(),
|
|
6208
6426
|
pushedBy: userSchema,
|
|
6427
|
+
/** The tool the person used (agentLabel); null: the website. Missing from older servers. */
|
|
6428
|
+
via: string().nullable().optional(),
|
|
6429
|
+
/** The project it belongs to, when the client said. */
|
|
6430
|
+
project: string().nullable().optional(),
|
|
6209
6431
|
added: number$1().int(),
|
|
6210
6432
|
changed: number$1().int(),
|
|
6211
6433
|
removed: number$1().int(),
|
|
@@ -6244,6 +6466,7 @@ const fileHistoryOutput = object({ entries: array(object({
|
|
|
6244
6466
|
]),
|
|
6245
6467
|
sha256: string().nullable(),
|
|
6246
6468
|
pushedBy: userSchema,
|
|
6469
|
+
via: string().nullable().optional(),
|
|
6247
6470
|
createdAt: number$1()
|
|
6248
6471
|
})) });
|
|
6249
6472
|
const headOutput = object({
|
|
@@ -6531,6 +6754,11 @@ const AGENT_NAMES = {
|
|
|
6531
6754
|
};
|
|
6532
6755
|
/** "claude-code" -> "Claude Code"; unknown ids are shown as-is. */
|
|
6533
6756
|
const agentLabel = (via) => AGENT_NAMES[via] ?? via;
|
|
6757
|
+
/**
|
|
6758
|
+
* Whether `via` is the person themselves rather than an agent acting for them: the website (null), the CLI typed by
|
|
6759
|
+
* hand ("cli"), or our own mobile app ("mobile"). Their reads count as reading, and notifications don't add "via ...".
|
|
6760
|
+
*/
|
|
6761
|
+
const isPersonVia = (via) => via === null || via === "cli" || via === "mobile";
|
|
6534
6762
|
const discordId = string().regex(/^\d{5,25}$/);
|
|
6535
6763
|
const discordName = string().trim().min(1).max(100);
|
|
6536
6764
|
const discordPersonSchema = object({
|
|
@@ -6729,6 +6957,13 @@ const discordChatOutput = object({
|
|
|
6729
6957
|
liveUrl: string().optional()
|
|
6730
6958
|
});
|
|
6731
6959
|
object({ messages: array(discordMessageSchema) });
|
|
6960
|
+
/**
|
|
6961
|
+
* A person and the tool they used, for lines that say who did something: "Jack (with Claude)". The person is always
|
|
6962
|
+
* the subject; their own hands (the website, the CLI typed by hand, the mobile app) add nothing.
|
|
6963
|
+
*/
|
|
6964
|
+
function byPerson(name, via) {
|
|
6965
|
+
return via && !isPersonVia(via) ? `${name} (with ${agentLabel(via)})` : name;
|
|
6966
|
+
}
|
|
6732
6967
|
//#endregion
|
|
6733
6968
|
//#region ../contract/src/sources.ts
|
|
6734
6969
|
const MAX_SOURCE_BYTES = 26214400;
|
|
@@ -7216,11 +7451,33 @@ function createClient(opts) {
|
|
|
7216
7451
|
removeMember: (slug, handle) => call("DELETE", `/v1/workspaces/${enc$3(slug)}/members/${enc$3(handle)}`, null),
|
|
7217
7452
|
acceptInvite: (slug) => call("POST", `/v1/workspaces/${enc$3(slug)}/accept`, null),
|
|
7218
7453
|
declineInvite: (slug) => call("POST", `/v1/workspaces/${enc$3(slug)}/decline`, null),
|
|
7219
|
-
listSpaces: (query = {}) =>
|
|
7454
|
+
listSpaces: (query = {}) => {
|
|
7455
|
+
const params = new URLSearchParams();
|
|
7456
|
+
if (query.workspace) params.set("workspace", query.workspace);
|
|
7457
|
+
if (query.status && query.status !== "active") params.set("status", query.status);
|
|
7458
|
+
const qs = params.toString();
|
|
7459
|
+
return call("GET", `/v1/spaces${qs ? `?${qs}` : ""}`, listSpacesOutput);
|
|
7460
|
+
},
|
|
7461
|
+
findSpaces: (query) => {
|
|
7462
|
+
const params = new URLSearchParams({ q: query.q });
|
|
7463
|
+
if (query.workspace) params.set("workspace", query.workspace);
|
|
7464
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
7465
|
+
return call("GET", `/v1/spaces/find?${params}`, findSpacesOutput);
|
|
7466
|
+
},
|
|
7220
7467
|
createSpace: (input) => call("POST", "/v1/spaces", spaceSchema, input),
|
|
7221
7468
|
getSpace: (id) => call("GET", space(id), spaceSchema),
|
|
7222
7469
|
updateSpace: (id, input) => call("PATCH", space(id), spaceSchema, input),
|
|
7223
|
-
deleteSpace: (id) => call("DELETE", space(id)
|
|
7470
|
+
deleteSpace: (id, opts = {}) => call("DELETE", `${space(id)}${opts.forever ? "?forever=1" : ""}`, null),
|
|
7471
|
+
restoreSpace: (id) => call("POST", `${space(id)}/restore`, spaceSchema),
|
|
7472
|
+
archiveSpace: (id) => call("POST", `${space(id)}/archive`, spaceSchema),
|
|
7473
|
+
unarchiveSpace: (id) => call("POST", `${space(id)}/unarchive`, spaceSchema),
|
|
7474
|
+
favoriteSpace: (id) => call("PUT", `${space(id)}/favorite`, null),
|
|
7475
|
+
fileSpace: (id, folderId) => call("PUT", `${space(id)}/folder`, spaceSchema, { folderId }),
|
|
7476
|
+
/** Puts this space into another as a page (Tidy up); it's archived and its links open the page there. */
|
|
7477
|
+
mergeSpace: (id, into) => call("POST", `${space(id)}/merge`, mergeSpaceOutput, { into }),
|
|
7478
|
+
unfavoriteSpace: (id) => call("DELETE", `${space(id)}/favorite`, null),
|
|
7479
|
+
orderFavorites: (input) => call("PUT", "/v1/me/favorites", favoritesOutput, input),
|
|
7480
|
+
openedSpace: (id) => call("POST", `${space(id)}/opened`, null),
|
|
7224
7481
|
moveSpace: (id, input) => call("POST", `${space(id)}/move`, moveSpaceOutput, input),
|
|
7225
7482
|
head: (id) => call("GET", `${space(id)}/head`, headOutput),
|
|
7226
7483
|
unlockSpace: (id, input) => call("POST", `${space(id)}/unlock`, unlockSpaceOutput, input),
|
|
@@ -7326,6 +7583,22 @@ function createClient(opts) {
|
|
|
7326
7583
|
listDiscordLinks: (spaceId) => call("GET", `/v1/spaces/${enc$3(spaceId)}/discord`, listDiscordLinksOutput),
|
|
7327
7584
|
discordChat: (spaceId, threadId, all = false) => call("GET", `/v1/spaces/${enc$3(spaceId)}/discord/${enc$3(threadId)}${all ? "?all=1" : ""}`, discordChatOutput),
|
|
7328
7585
|
unlinkDiscordThread: (spaceId, threadId) => send("DELETE", `/v1/spaces/${enc$3(spaceId)}/discord/${enc$3(threadId)}`),
|
|
7586
|
+
listOpenableSpaces: () => call("GET", "/v1/live-spaces", listOpenableSpacesOutput),
|
|
7587
|
+
listActivity: (slug, query = {}) => {
|
|
7588
|
+
const q = new URLSearchParams({ v: "2" });
|
|
7589
|
+
if (query.person) q.set("person", query.person);
|
|
7590
|
+
if (query.before) q.set("before", String(query.before));
|
|
7591
|
+
if (query.before && query.beforeId) q.set("beforeId", query.beforeId);
|
|
7592
|
+
if (query.limit) q.set("limit", String(query.limit));
|
|
7593
|
+
return call("GET", `/v1/workspaces/${enc$3(slug)}/activity?${q}`, listActivityOutput);
|
|
7594
|
+
},
|
|
7595
|
+
listFolders: (slug) => call("GET", `/v1/workspaces/${enc$3(slug)}/folders`, listFoldersOutput),
|
|
7596
|
+
/** Tidy up: your look-alike spaces that could be one, and the ones that look finished. */
|
|
7597
|
+
tidy: (slug) => call("GET", `/v1/workspaces/${enc$3(slug)}/tidy`, tidyOutput),
|
|
7598
|
+
createFolder: (slug, input) => call("POST", `/v1/workspaces/${enc$3(slug)}/folders`, folderSchema, input),
|
|
7599
|
+
updateFolder: (slug, id, input) => call("PATCH", `/v1/workspaces/${enc$3(slug)}/folders/${enc$3(id)}`, folderSchema, input),
|
|
7600
|
+
deleteFolder: (slug, id) => call("DELETE", `/v1/workspaces/${enc$3(slug)}/folders/${enc$3(id)}`, null),
|
|
7601
|
+
undoActivity: (slug, eventId) => call("POST", `/v1/workspaces/${enc$3(slug)}/activity/${enc$3(eventId)}/undo`, undoActivityOutput),
|
|
7329
7602
|
listAudit: (slug, query = {}) => {
|
|
7330
7603
|
const q = new URLSearchParams();
|
|
7331
7604
|
if (query.before) q.set("before", String(query.before));
|
|
@@ -7337,6 +7610,82 @@ function createClient(opts) {
|
|
|
7337
7610
|
};
|
|
7338
7611
|
}
|
|
7339
7612
|
new TextEncoder();
|
|
7613
|
+
new Set("a an and the of for to in on at by with from new draft copy final v1 v2 v3 page pages html index untitled my our".split(" "));
|
|
7614
|
+
/** A folder name for a page added to a space: "Pricing proposal D.html" → "pricing-proposal-d". */
|
|
7615
|
+
function pageSlug(text) {
|
|
7616
|
+
return text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/\.html?$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/, "") || "page";
|
|
7617
|
+
}
|
|
7618
|
+
//#endregion
|
|
7619
|
+
//#region ../contract/src/folders.ts
|
|
7620
|
+
const norm = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
7621
|
+
const parts = (path) => path.split("/").map((p) => p.trim()).filter(Boolean);
|
|
7622
|
+
/** True for "/" and "": the top level, outside every folder. Words like "top" are folder names (one may be called Top). */
|
|
7623
|
+
const isTopLevel = (path) => ["", "/"].includes(norm(path));
|
|
7624
|
+
/**
|
|
7625
|
+
* The folder a path names: its id, its whole path (any case), or a name only one folder has. Null when none does or
|
|
7626
|
+
* when a bare name fits several (say which with the whole path).
|
|
7627
|
+
*/
|
|
7628
|
+
function findFolder(folders, path) {
|
|
7629
|
+
const byId = folders.find((f) => f.id === path.trim());
|
|
7630
|
+
if (byId) return byId;
|
|
7631
|
+
const want = parts(path).map(norm).join("/");
|
|
7632
|
+
const exact = folders.find((f) => norm(f.path) === want);
|
|
7633
|
+
if (exact) return exact;
|
|
7634
|
+
if (want.includes("/")) return null;
|
|
7635
|
+
const named = folders.filter((f) => norm(f.name) === want);
|
|
7636
|
+
return named.length === 1 ? named[0] ?? null : null;
|
|
7637
|
+
}
|
|
7638
|
+
/** Folders whose path looks like what was typed, best first: for "no folder called …, did you mean …". */
|
|
7639
|
+
function closeFolders(folders, path, limit = 3) {
|
|
7640
|
+
const words = norm(path).split(/[\s/]+/).filter(Boolean);
|
|
7641
|
+
return folders.map((f) => {
|
|
7642
|
+
const p = norm(f.path);
|
|
7643
|
+
return {
|
|
7644
|
+
f,
|
|
7645
|
+
score: words.filter((w) => p.includes(w)).length + (p.includes(norm(path)) ? 2 : 0)
|
|
7646
|
+
};
|
|
7647
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || a.f.path.length - b.f.path.length).slice(0, limit).map((x) => x.f);
|
|
7648
|
+
}
|
|
7649
|
+
/** The message when a path names no folder: close matches, and how to make it. */
|
|
7650
|
+
function noFolderMessage(folders, path, make) {
|
|
7651
|
+
const near = closeFolders(folders, path);
|
|
7652
|
+
const named = folders.filter((f) => norm(f.name) === norm(path));
|
|
7653
|
+
if (named.length > 1) return `More than one folder is called "${path.trim()}": ${named.map((f) => `"${f.path}"`).join(", ")}. Give the whole path.`;
|
|
7654
|
+
return [
|
|
7655
|
+
`No folder called "${path.trim()}".`,
|
|
7656
|
+
near.length ? `Close: ${near.map((f) => `"${f.path}"`).join(", ")}.` : "",
|
|
7657
|
+
`To make it: ${make}`
|
|
7658
|
+
].filter(Boolean).join(" ");
|
|
7659
|
+
}
|
|
7660
|
+
/**
|
|
7661
|
+
* Makes the folders a path names that don't exist yet, top down ("Deals/Enterprise" makes Deals if needed, then
|
|
7662
|
+
* Enterprise in it), and returns the last one, plus which were new.
|
|
7663
|
+
*/
|
|
7664
|
+
async function ensureFolderPath(client, slug, path, description) {
|
|
7665
|
+
let folders = (await client.listFolders(slug)).folders;
|
|
7666
|
+
const names = parts(path);
|
|
7667
|
+
if (!names.length) throw new Error("Give the folder a name.");
|
|
7668
|
+
const made = [];
|
|
7669
|
+
let parent = null;
|
|
7670
|
+
for (const [i, name] of names.entries()) {
|
|
7671
|
+
const sofar = names.slice(0, i + 1).map(norm).join("/");
|
|
7672
|
+
let at = folders.find((f) => norm(f.path) === sofar) ?? null;
|
|
7673
|
+
if (!at) {
|
|
7674
|
+
at = await client.createFolder(slug, {
|
|
7675
|
+
name,
|
|
7676
|
+
parentId: parent?.id ?? null,
|
|
7677
|
+
...i === names.length - 1 && description ? { description } : {}
|
|
7678
|
+
});
|
|
7679
|
+
made.push(at);
|
|
7680
|
+
folders = [...folders, at];
|
|
7681
|
+
}
|
|
7682
|
+
parent = at;
|
|
7683
|
+
}
|
|
7684
|
+
return {
|
|
7685
|
+
folder: parent,
|
|
7686
|
+
made
|
|
7687
|
+
};
|
|
7688
|
+
}
|
|
7340
7689
|
//#endregion
|
|
7341
7690
|
//#region ../contract/src/guest-writes.ts
|
|
7342
7691
|
/**
|
|
@@ -11072,6 +11421,29 @@ function findInitRoot(from = process.cwd()) {
|
|
|
11072
11421
|
dir = parent;
|
|
11073
11422
|
}
|
|
11074
11423
|
}
|
|
11424
|
+
/**
|
|
11425
|
+
* The project work belongs to, recorded on each version: CLOUDTIFACT_PROJECT, else the name of the git repo above
|
|
11426
|
+
* `from` (a worktree counts as its main repo), else null.
|
|
11427
|
+
*/
|
|
11428
|
+
function projectName(from = process.cwd(), env = process.env) {
|
|
11429
|
+
const named = env.CLOUDTIFACT_PROJECT?.trim();
|
|
11430
|
+
if (named) return named.slice(0, 100);
|
|
11431
|
+
let dir = path.resolve(from);
|
|
11432
|
+
for (;;) {
|
|
11433
|
+
const git = path.join(dir, ".git");
|
|
11434
|
+
if (existsSync(git)) {
|
|
11435
|
+
try {
|
|
11436
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync(git, "utf8"));
|
|
11437
|
+
const main = m?.[1] ? /^(.*)[\\/]\.git[\\/]worktrees[\\/]/.exec(m[1].trim()) : null;
|
|
11438
|
+
if (main?.[1]) return path.basename(main[1]);
|
|
11439
|
+
} catch {}
|
|
11440
|
+
return path.basename(dir);
|
|
11441
|
+
}
|
|
11442
|
+
const parent = path.dirname(dir);
|
|
11443
|
+
if (parent === dir) return null;
|
|
11444
|
+
dir = parent;
|
|
11445
|
+
}
|
|
11446
|
+
}
|
|
11075
11447
|
function parseRepoConfig(raw, file = REPO_CONFIG_FILE) {
|
|
11076
11448
|
const bad = (why) => /* @__PURE__ */ new Error(`${file}: ${why}`);
|
|
11077
11449
|
if (!raw || typeof raw !== "object") throw bad("expected a JSON object");
|
|
@@ -11226,6 +11598,8 @@ function versionsData(versions) {
|
|
|
11226
11598
|
version: v.number,
|
|
11227
11599
|
at: iso(v.createdAt),
|
|
11228
11600
|
by: v.pushedBy.handle,
|
|
11601
|
+
via: v.via ?? null,
|
|
11602
|
+
project: v.project ?? null,
|
|
11229
11603
|
added: v.added,
|
|
11230
11604
|
changed: v.changed,
|
|
11231
11605
|
removed: v.removed,
|
|
@@ -11735,7 +12109,10 @@ async function pushTarget(client, target, initial, opts = {}) {
|
|
|
11735
12109
|
const planAndUpload = async () => {
|
|
11736
12110
|
const manifest = manifestOf(files);
|
|
11737
12111
|
const bySha = new Map(files.map((f) => [f.sha256, f]));
|
|
11738
|
-
const plan = await client.pushPlan(target.spaceId, {
|
|
12112
|
+
const plan = await client.pushPlan(target.spaceId, {
|
|
12113
|
+
files: manifest,
|
|
12114
|
+
...opts.prefix ? { prefix: opts.prefix } : {}
|
|
12115
|
+
});
|
|
11739
12116
|
if (opts.dryRun || isEmptyDiff(plan.diff)) return {
|
|
11740
12117
|
plan,
|
|
11741
12118
|
manifest
|
|
@@ -11767,7 +12144,9 @@ async function pushTarget(client, target, initial, opts = {}) {
|
|
|
11767
12144
|
result = await client.createVersion(target.spaceId, {
|
|
11768
12145
|
files: manifest,
|
|
11769
12146
|
message: opts.message,
|
|
11770
|
-
baseVersion: plan.baseVersion
|
|
12147
|
+
baseVersion: plan.baseVersion,
|
|
12148
|
+
...opts.prefix ? { prefix: opts.prefix } : {},
|
|
12149
|
+
...opts.project ? { project: opts.project } : {}
|
|
11771
12150
|
});
|
|
11772
12151
|
} catch (err) {
|
|
11773
12152
|
if (!(err instanceof ApiError && err.status === 409)) throw err;
|
|
@@ -11781,7 +12160,9 @@ async function pushTarget(client, target, initial, opts = {}) {
|
|
|
11781
12160
|
result = await client.createVersion(target.spaceId, {
|
|
11782
12161
|
files: manifest,
|
|
11783
12162
|
message: opts.message,
|
|
11784
|
-
baseVersion: plan.baseVersion
|
|
12163
|
+
baseVersion: plan.baseVersion,
|
|
12164
|
+
...opts.prefix ? { prefix: opts.prefix } : {},
|
|
12165
|
+
...opts.project ? { project: opts.project } : {}
|
|
11785
12166
|
});
|
|
11786
12167
|
}
|
|
11787
12168
|
if (!result.version) return {
|
|
@@ -11820,6 +12201,7 @@ async function pushRepo(client, repo, opts = {}) {
|
|
|
11820
12201
|
});
|
|
11821
12202
|
const r = await pushTarget(client, target, files, {
|
|
11822
12203
|
...opts,
|
|
12204
|
+
project: opts.project ?? projectName(repo.root),
|
|
11823
12205
|
recollect: async () => (await collect(repo.docsDir)).files
|
|
11824
12206
|
});
|
|
11825
12207
|
results.push(r);
|
|
@@ -11860,9 +12242,10 @@ function commentsUrl(shareUrl) {
|
|
|
11860
12242
|
* app, with a Comment button in the corner) and with the comment panel already open. A private page has only the
|
|
11861
12243
|
* app link, which members open inside the Cloudtifact viewer.
|
|
11862
12244
|
*/
|
|
11863
|
-
function linkLines(space) {
|
|
11864
|
-
|
|
11865
|
-
return [`
|
|
12245
|
+
function linkLines(space, prefix) {
|
|
12246
|
+
const share = space.shareUrl && prefix ? space.shareUrl + prefix : space.shareUrl;
|
|
12247
|
+
if (share) return [`Page: ${share}`, `With comments: ${commentsUrl(share)}`];
|
|
12248
|
+
return [`Link: ${prefix ? `${space.url}/${prefix}index.html` : space.url}`, `Private: only members of the ${space.workspace.slug} workspace can open it. To make it public, change visibility in the space settings on the website.`];
|
|
11866
12249
|
}
|
|
11867
12250
|
/** A local, relative reference, or null for absolute URLs, data:, anchors, etc. Drops ?query and #hash. */
|
|
11868
12251
|
function localRef(raw) {
|
|
@@ -11986,7 +12369,44 @@ async function publishToSpace(client, spaceId, bundle, opts) {
|
|
|
11986
12369
|
});
|
|
11987
12370
|
await applyGuestWrites(client, spaceId, opts.folder, { log: opts.log });
|
|
11988
12371
|
}
|
|
11989
|
-
|
|
12372
|
+
const prefix = opts.prefix;
|
|
12373
|
+
const files = prefix ? bundle.files.map((f) => ({
|
|
12374
|
+
...f,
|
|
12375
|
+
path: prefix + f.path
|
|
12376
|
+
})) : bundle.files;
|
|
12377
|
+
return pushTarget(client, { spaceId }, files, {
|
|
12378
|
+
message: opts.message,
|
|
12379
|
+
prefix,
|
|
12380
|
+
project: opts.project
|
|
12381
|
+
});
|
|
12382
|
+
}
|
|
12383
|
+
/**
|
|
12384
|
+
* The space new work belongs in: an active space in the workspace you can publish to, whose name matches the page
|
|
12385
|
+
* strongly (topicMatch), with the same who-can-see as asked for. A private page never lands in a public space, and a
|
|
12386
|
+
* public one never in a private space: publishing doesn't change who can see anything. Null: make a new space.
|
|
12387
|
+
*/
|
|
12388
|
+
async function findHome(client, title, opts) {
|
|
12389
|
+
return (await client.findSpaces({
|
|
12390
|
+
q: title,
|
|
12391
|
+
workspace: opts.workspace,
|
|
12392
|
+
limit: 10
|
|
12393
|
+
}).catch((e) => {
|
|
12394
|
+
if (e instanceof ApiError && (e.status === 404 || e.status === 400)) return { spaces: [] };
|
|
12395
|
+
throw e;
|
|
12396
|
+
})).spaces.find((s) => s.strong && s.kind === "repo" && s.permissions.canPush && !s.passwordProtected && s.visibility === "public" === opts.public) ?? null;
|
|
12397
|
+
}
|
|
12398
|
+
/** A folder for the page in the space that no other page uses: "pricing-d/", else "pricing-d-2/", ... */
|
|
12399
|
+
async function freePrefix(client, space, name) {
|
|
12400
|
+
const taken = /* @__PURE__ */ new Set();
|
|
12401
|
+
if (space.latestVersion > 0) {
|
|
12402
|
+
const latest = await client.getVersion(space.id, "latest");
|
|
12403
|
+
for (const f of latest.files) taken.add(f.path.split("/")[0] ?? "");
|
|
12404
|
+
}
|
|
12405
|
+
const base = pageSlug(name);
|
|
12406
|
+
for (let i = 1;; i++) {
|
|
12407
|
+
const slug = i === 1 ? base : `${base}-${i}`;
|
|
12408
|
+
if (!taken.has(slug) && !taken.has(`${slug}.html`)) return `${slug}/`;
|
|
12409
|
+
}
|
|
11990
12410
|
}
|
|
11991
12411
|
const artifactsPath = (env = process.env) => path.join(globalConfigDir(env), "artifacts.json");
|
|
11992
12412
|
async function readArtifacts(env = process.env) {
|
|
@@ -12062,7 +12482,7 @@ function tokenSpace(token) {
|
|
|
12062
12482
|
//#endregion
|
|
12063
12483
|
//#region src/version.ts
|
|
12064
12484
|
/** This CLI's version, from package.json at build time (tsdown.config.ts). Unbuilt (tests, tsx): "0.0.0-dev". */
|
|
12065
|
-
const VERSION = "0.2.
|
|
12485
|
+
const VERSION = "0.2.4";
|
|
12066
12486
|
/**
|
|
12067
12487
|
* Sent on every API request, so the API can tell which CLI version is calling (and one day tell an old one to
|
|
12068
12488
|
* upgrade). Without it Node sends just "node", which is what 0.2.2 and older send. The sign-in page shows it too.
|
|
@@ -12343,6 +12763,25 @@ function chatsToMarkdown(spaceName, chats) {
|
|
|
12343
12763
|
return `${out.join("\n").trimEnd()}\n`;
|
|
12344
12764
|
}
|
|
12345
12765
|
//#endregion
|
|
12766
|
+
//#region src/folders.ts
|
|
12767
|
+
/** The folder a path names, or null for "/" (the top level). Unknown paths fail with close matches and the fix. */
|
|
12768
|
+
async function folderFor(client, slug, path) {
|
|
12769
|
+
if (isTopLevel(path)) return null;
|
|
12770
|
+
const { folders } = await client.listFolders(slug);
|
|
12771
|
+
const f = findFolder(folders, path);
|
|
12772
|
+
if (!f) throw new CliError(noFolderMessage(folders, path, `cloudtifact folder create "${path.trim()}"`));
|
|
12773
|
+
return f;
|
|
12774
|
+
}
|
|
12775
|
+
/** "Product plans › Pricing": a path as people read it. */
|
|
12776
|
+
const shownPath = (f) => f ? f.path.replaceAll("/", " › ") : "the top level";
|
|
12777
|
+
/** The tree as indented lines: name, how many spaces, what it's for. */
|
|
12778
|
+
function treeLines(folders) {
|
|
12779
|
+
return folders.map((f) => {
|
|
12780
|
+
const depth = f.path.split("/").length - 1;
|
|
12781
|
+
return `${" ".repeat(depth)}▤ ${f.name} ${f.spaces}${f.description ? ` · ${f.description}` : ""}`;
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
//#endregion
|
|
12346
12785
|
//#region src/hooks.ts
|
|
12347
12786
|
const SCRIPT_INSTALL_BIN = "\"$HOME/.cloudtifact/bin/cloudtifact\"";
|
|
12348
12787
|
/** The command every harness runs after a file edit. Reads the hook payload on stdin, always exits 0. */
|
|
@@ -12779,6 +13218,7 @@ When you make an HTML page for the user, run \`cloudtifact publish <file.html>\`
|
|
|
12779
13218
|
- To update it, edit the same file and publish it again: new version, same link.
|
|
12780
13219
|
- Keep supporting files next to the HTML (or in subfolders) and reference them with relative paths (\`style.css\`, \`img/logo.png\`). Absolute paths and files outside the page's folder aren't included.
|
|
12781
13220
|
- Pages are private by default; add \`--public\` on the first publish for a link anyone can open. \`cloudtifact artifacts\` lists what you've published.
|
|
13221
|
+
- New work joins the space already about the same topic, as a page, instead of making another space: \`publish\` finds it (same workspace, same who-can-see) and prints \`Looks like "<space>"\`. Tell the user where it went. To check first, run \`cloudtifact find "<title>" --toon\`; \`--add-to <id>\` adds the page to a space you pick, \`--new\` makes a separate space when the user wants one.
|
|
12782
13222
|
|
|
12783
13223
|
## Space files
|
|
12784
13224
|
|
|
@@ -12788,6 +13228,18 @@ People can add files to a space for the work (chat exports, emails, PDFs, spread
|
|
|
12788
13228
|
|
|
12789
13229
|
Spaces live in workspaces (personal or team; the user can belong to several). \`cloudtifact publish\` and \`cloudtifact init\` create new spaces in the current workspace and print which one; pass \`--workspace <slug>\` to pick another. \`cloudtifact workspace list\` shows the user's workspaces (current one marked); \`cloudtifact workspace use <slug>\` switches the default. Pushing to an existing space doesn't need a workspace. Only change the default when the user asks.
|
|
12790
13230
|
|
|
13231
|
+
## Folders
|
|
13232
|
+
|
|
13233
|
+
A workspace's spaces can sit in folders (any depth, like "Deals/Enterprise"); everyone in it shares them. \`cloudtifact folders --toon\` lists every path, how many spaces it holds and what goes there: read the descriptions before filing.
|
|
13234
|
+
|
|
13235
|
+
- File a new space with \`publish --folder "<path>"\` when the user named a folder, \`.cloudtifact.json\` says one, or a folder's description clearly says it's where this goes. Otherwise leave it out and say where it went. An unknown folder fails with close matches: don't make one to get past it.
|
|
13236
|
+
- When the user asks, you can make, rename, describe and move folders and move spaces: \`cloudtifact folder create "<path>" [--description "<what goes here>"]\` (makes the folders the path needs), \`folder rename <path> <name>\`, \`folder describe <path> "<text>"\`, \`folder move <path> <to-path or />\`, and \`cloudtifact move <space> <folder-path or />\`. Never on your own: suggest it and let the user decide.
|
|
13237
|
+
- Every change is in the workspace's Activity under the user's name, with Undo (\`cloudtifact activity\`, \`cloudtifact undo <id>\`). Moving never changes a space's link or who can open it. Only the person can remove a folder, in the Cloudtifact app.
|
|
13238
|
+
|
|
13239
|
+
## Putting spaces away
|
|
13240
|
+
|
|
13241
|
+
When the user asks to archive, clear out or tidy up spaces, run \`cloudtifact archive <id>\`: the space leaves every list while its link, comments and data keep working, and \`cloudtifact unarchive <id>\` (or publishing to it again) brings it back. Never try to delete a space: only the person can, in the Cloudtifact app. \`cloudtifact favorite <id>\` pins a space at the top of the user's own sidebar.
|
|
13242
|
+
|
|
12791
13243
|
## Space databases
|
|
12792
13244
|
|
|
12793
13245
|
Every space has its own SQLite database, shared by all its pages. Commands take \`--space <id>\` (default: the first target in \`.cloudtifact.json\`) and \`--toon\` / \`--json\`:
|
|
@@ -13212,7 +13664,7 @@ const program = new Command().name("cloudtifact").description("Publish pages and
|
|
|
13212
13664
|
const tty = !!process.stdout.isTTY && !!process.stderr.isTTY;
|
|
13213
13665
|
if (updateNoticeOff({
|
|
13214
13666
|
env: process.env,
|
|
13215
|
-
current: "0.2.
|
|
13667
|
+
current: "0.2.4",
|
|
13216
13668
|
format,
|
|
13217
13669
|
command: cmd.name(),
|
|
13218
13670
|
tty
|
|
@@ -13500,7 +13952,7 @@ program.command("init").description("Link this repo to a space (creating one in
|
|
|
13500
13952
|
console.log("Next: `cloudtifact push`");
|
|
13501
13953
|
if (opts.hooks) await hooksInstall(root, {});
|
|
13502
13954
|
}));
|
|
13503
|
-
program.command("push").description("Upload changed files and record a new version on every target").option("-m, --message <message>", "version message").option("--dry-run", "show what would change, upload nothing").option("-q, --quiet", "print nothing unless something fails").option("--no-db", "don't run the docs folder's db/*.sql files").addOption(new Option("--if-changed-path <file>", "only push if this file is inside the docs folder").hideHelp()).action(run(async (opts) => {
|
|
13955
|
+
program.command("push").description("Upload changed files and record a new version on every target").option("-m, --message <message>", "version message").option("--dry-run", "show what would change, upload nothing").option("-q, --quiet", "print nothing unless something fails").option("--no-db", "don't run the docs folder's db/*.sql files").option("--project <name>", "the project this work belongs to (default: the git repo's name)").addOption(new Option("--if-changed-path <file>", "only push if this file is inside the docs folder").hideHelp()).action(run(async (opts) => {
|
|
13504
13956
|
const repo = await loadRepo();
|
|
13505
13957
|
if (opts.ifChangedPath && !isInside(repo.docsDir, opts.ifChangedPath)) return;
|
|
13506
13958
|
const { client } = await getClient();
|
|
@@ -13508,6 +13960,7 @@ program.command("push").description("Upload changed files and record a new versi
|
|
|
13508
13960
|
message: opts.message,
|
|
13509
13961
|
dryRun: opts.dryRun,
|
|
13510
13962
|
db: opts.db,
|
|
13963
|
+
project: opts.project,
|
|
13511
13964
|
log: opts.quiet ? void 0 : (l) => console.log(l)
|
|
13512
13965
|
})).some((r) => r.status === "error")) process.exitCode = 1;
|
|
13513
13966
|
}));
|
|
@@ -13722,7 +14175,9 @@ for (const [name, verb] of [["resolve", "Resolved"], ["unresolve", "Reopened"]])
|
|
|
13722
14175
|
const c = await (name === "resolve" ? client.resolveComment(commentId) : client.unresolveComment(commentId));
|
|
13723
14176
|
console.log(`${verb} ${c.id} on ${c.path}.`);
|
|
13724
14177
|
}));
|
|
13725
|
-
program.command("publish").description("Publish an HTML page (with the files it references) or a folder; prints its link").argument("<path>", "an .html file or a folder with index.html").option("--public", "anyone with the link can view (only when first published; default: private)").option("--name <name>", "space name (default: the page's <title>, else the file name)").option("--workspace <slug>", "workspace for a new space (default: the current one, see `workspace list`)").option("--space <id>", "publish
|
|
14178
|
+
program.command("publish").description("Publish an HTML page (with the files it references) or a folder; prints its link").argument("<path>", "an .html file or a folder with index.html").option("--public", "anyone with the link can view (only when first published; default: private)").option("--name <name>", "space name (default: the page's <title>, else the file name)").option("--workspace <slug>", "workspace for a new space (default: the current one, see `workspace list`)").option("--space <id>", "publish as this existing space's main page (id or link), replacing its files").option("--add-to <id>", "add it as a page in this existing space (id or link), keeping the space's other pages").option("--new", "make a new space, even when an existing one looks like the same topic").option("--folder <path>", "file a new space in this folder, like \"Deals/Enterprise\" (see `cloudtifact folders`)").option("--project <name>", "the project this work belongs to (default: the git repo it's in)").option("--entry <file>", "folder only: the page to publish as index.html").option("-m, --message <message>", "version message").option("--no-db", "don't run the db/*.sql files next to the page").action(run(async (target, opts) => {
|
|
14179
|
+
if (opts.space && opts.addTo) throw new CliError("Pass --space or --add-to, not both.");
|
|
14180
|
+
if ((opts.space || opts.addTo) && opts.new) throw new CliError("--new makes a new space; drop --space/--add-to.");
|
|
13726
14181
|
const abs = path.resolve(target);
|
|
13727
14182
|
if (!existsSync(abs)) throw new CliError(`${target} does not exist.`);
|
|
13728
14183
|
const isDir = statSync(abs).isDirectory();
|
|
@@ -13733,32 +14188,78 @@ program.command("publish").description("Publish an HTML page (with the files it
|
|
|
13733
14188
|
const { client, baseUrl, config, account } = await getClient();
|
|
13734
14189
|
const saved = lookupArtifact(await readArtifacts(), abs, baseUrl);
|
|
13735
14190
|
let space = null;
|
|
13736
|
-
|
|
14191
|
+
let prefix;
|
|
14192
|
+
if (opts.addTo) {
|
|
14193
|
+
space = await client.getSpace(spaceIdOrThrow(opts.addTo));
|
|
14194
|
+
prefix = saved?.spaceId === space.id && saved.prefix ? saved.prefix : await freePrefix(client, space, path.basename(abs));
|
|
14195
|
+
if (saved?.spaceId !== space.id || saved.prefix !== prefix) await saveArtifact(abs, {
|
|
14196
|
+
spaceId: space.id,
|
|
14197
|
+
apiUrl: baseUrl,
|
|
14198
|
+
createdAt: Date.now(),
|
|
14199
|
+
prefix,
|
|
14200
|
+
...account ? { handle: account.handle } : {}
|
|
14201
|
+
});
|
|
14202
|
+
} else if (opts.space) {
|
|
13737
14203
|
space = await client.getSpace(spaceIdOrThrow(opts.space));
|
|
13738
14204
|
if (saved?.spaceId !== space.id) await saveArtifact(abs, {
|
|
13739
14205
|
spaceId: space.id,
|
|
13740
14206
|
apiUrl: baseUrl,
|
|
13741
14207
|
createdAt: Date.now()
|
|
13742
14208
|
});
|
|
13743
|
-
} else if (saved) {
|
|
14209
|
+
} else if (saved && !opts.new) {
|
|
13744
14210
|
space = await client.getSpace(saved.spaceId).catch((e) => {
|
|
13745
14211
|
if (e instanceof ApiError && e.status === 404) return null;
|
|
13746
14212
|
throw e;
|
|
13747
14213
|
});
|
|
13748
14214
|
if (!space && saved.handle && account && saved.handle !== account.handle) throw new CliError(`This page was published as @${saved.handle} (space ${saved.spaceId}); you don't have access as @${account.handle}. Run it with --account ${saved.handle}.`);
|
|
14215
|
+
if (space) prefix = saved.prefix;
|
|
13749
14216
|
if (!space) console.error(`Space ${saved.spaceId} is gone; creating a new one.`);
|
|
13750
14217
|
else if (opts.workspace && opts.workspace !== space.workspace.slug) console.error(`note: this page is already published in ${space.workspace.slug}; --workspace only applies to new spaces.`);
|
|
13751
14218
|
else if (opts.public && !space.shareUrl) console.error("note: --public only applies when first published; this artifact stays private.");
|
|
13752
14219
|
}
|
|
14220
|
+
let newIn;
|
|
14221
|
+
const newSpaceWorkspace = async () => newIn ??= await workspaceForNewSpace(client, opts.workspace, config.workspace);
|
|
13753
14222
|
if (!space) {
|
|
13754
14223
|
const name = opts.name ?? bundle.title ?? path.basename(abs);
|
|
13755
|
-
const cur = await
|
|
14224
|
+
const cur = await newSpaceWorkspace();
|
|
13756
14225
|
console.error(workspaceLine(cur));
|
|
14226
|
+
const home = opts.new ? null : await findHome(client, name, {
|
|
14227
|
+
workspace: cur.workspace.slug,
|
|
14228
|
+
public: !!opts.public
|
|
14229
|
+
});
|
|
14230
|
+
if (home) {
|
|
14231
|
+
space = await client.getSpace(home.id);
|
|
14232
|
+
prefix = await freePrefix(client, space, path.basename(abs));
|
|
14233
|
+
console.error(`Looks like "${space.name}" (${space.id}): adding it there as a page. Use --new for a separate space.`);
|
|
14234
|
+
await saveArtifact(abs, {
|
|
14235
|
+
spaceId: space.id,
|
|
14236
|
+
apiUrl: baseUrl,
|
|
14237
|
+
createdAt: Date.now(),
|
|
14238
|
+
prefix,
|
|
14239
|
+
...account ? { handle: account.handle } : {}
|
|
14240
|
+
});
|
|
14241
|
+
}
|
|
14242
|
+
}
|
|
14243
|
+
let filed;
|
|
14244
|
+
if (opts.folder) {
|
|
14245
|
+
if (space && prefix !== void 0) console.error(`note: it's a page in "${space.name}", so it stays where that space is; --folder was not used.`);
|
|
14246
|
+
else if (space) console.error(`note: "${space.name}" is already published; --folder only files a new space. To move it: cloudtifact move ${space.id} "${opts.folder}"`);
|
|
14247
|
+
else filed = await folderFor(client, (await newSpaceWorkspace()).workspace.slug, opts.folder).catch((e) => {
|
|
14248
|
+
if (!(e instanceof CliError)) throw e;
|
|
14249
|
+
console.error(`warning: ${e.message}; publishing it at the top level instead.`);
|
|
14250
|
+
return null;
|
|
14251
|
+
});
|
|
14252
|
+
}
|
|
14253
|
+
if (!space) {
|
|
14254
|
+
const name = opts.name ?? bundle.title ?? path.basename(abs);
|
|
14255
|
+
const cur = await newSpaceWorkspace();
|
|
13757
14256
|
space = await client.createSpace({
|
|
13758
14257
|
workspace: cur.workspace.slug,
|
|
13759
14258
|
name,
|
|
13760
|
-
visibility: opts.public ? "public" : "members"
|
|
14259
|
+
visibility: opts.public ? "public" : "members",
|
|
14260
|
+
...filed ? { folderId: filed.id } : {}
|
|
13761
14261
|
});
|
|
14262
|
+
if (filed) console.error(`Filed in ${shownPath(filed)}`);
|
|
13762
14263
|
await saveArtifact(abs, {
|
|
13763
14264
|
spaceId: space.id,
|
|
13764
14265
|
apiUrl: baseUrl,
|
|
@@ -13768,6 +14269,8 @@ program.command("publish").description("Publish an HTML page (with the files it
|
|
|
13768
14269
|
}
|
|
13769
14270
|
const r = await publishToSpace(client, space.id, bundle, {
|
|
13770
14271
|
folder: pageFolder(abs, isDir),
|
|
14272
|
+
prefix,
|
|
14273
|
+
project: opts.project ?? projectName(path.dirname(abs)),
|
|
13771
14274
|
message: opts.message,
|
|
13772
14275
|
db: opts.db,
|
|
13773
14276
|
log: (l) => console.log(l),
|
|
@@ -13776,7 +14279,31 @@ program.command("publish").description("Publish an HTML page (with the files it
|
|
|
13776
14279
|
const n = bundle.files.length;
|
|
13777
14280
|
if (r.status === "pushed") console.log(`Published "${space.name}" v${r.version} (${n} file${n === 1 ? "" : "s"}; ${summarize(r.diff)})`);
|
|
13778
14281
|
else console.log(`No changes since v${space.latestVersion}: "${space.name}"`);
|
|
13779
|
-
for (const line of linkLines(space)) console.log(line);
|
|
14282
|
+
for (const line of linkLines(space, prefix)) console.log(line);
|
|
14283
|
+
}));
|
|
14284
|
+
program.command("find").description("Find the spaces a topic already lives in, best match first, so new work can go in as a page").argument("<words...>", "what it's about, like a title").option("--workspace <slug>", "only this workspace (default: all of yours)").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (words, opts) => {
|
|
14285
|
+
const { client } = await getClient();
|
|
14286
|
+
const { spaces } = await client.findSpaces({
|
|
14287
|
+
q: words.join(" "),
|
|
14288
|
+
workspace: opts.workspace,
|
|
14289
|
+
limit: 10
|
|
14290
|
+
});
|
|
14291
|
+
const format = resolveFormat(opts);
|
|
14292
|
+
const rows = spaces.map((s) => ({
|
|
14293
|
+
id: s.id,
|
|
14294
|
+
name: s.name,
|
|
14295
|
+
workspace: s.workspace.slug,
|
|
14296
|
+
pages: s.pages,
|
|
14297
|
+
match: s.strong ? "strong" : "weak",
|
|
14298
|
+
page: s.page,
|
|
14299
|
+
visibility: s.visibility,
|
|
14300
|
+
canPublish: s.permissions.canPush,
|
|
14301
|
+
link: s.shareUrl ?? s.url
|
|
14302
|
+
}));
|
|
14303
|
+
if (format !== "text") return printData(format, { spaces: rows });
|
|
14304
|
+
if (!rows.length) return console.log("Nothing matches. `cloudtifact publish` will make a new space.");
|
|
14305
|
+
for (const r of rows) console.log(`${r.id} ${r.name} ${r.pages} page${r.pages === 1 ? "" : "s"} · ${r.workspace}${r.match === "strong" ? " · same topic" : ""}${r.page ? ` · page "${r.page}"` : ""}`);
|
|
14306
|
+
console.log("Add a page to one: cloudtifact publish <file> --add-to <id>");
|
|
13780
14307
|
}));
|
|
13781
14308
|
program.command("artifacts").description("List pages published with `cloudtifact publish`").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
|
|
13782
14309
|
const map = await readArtifacts();
|
|
@@ -13816,7 +14343,7 @@ program.command("versions").description("List a space's versions, newest first")
|
|
|
13816
14343
|
versions: rows
|
|
13817
14344
|
});
|
|
13818
14345
|
if (!rows.length) return console.log("Nothing pushed yet.");
|
|
13819
|
-
for (const v of rows) console.log(`v${v.version} ${v.at} UTC
|
|
14346
|
+
for (const v of rows) console.log(`v${v.version} ${v.at} UTC ${byPerson(`@${v.by}`, v.via)}${v.project ? ` · ${v.project}` : ""} +${v.added} ~${v.changed} -${v.removed}${v.message ? ` ${v.message}` : ""}`);
|
|
13820
14347
|
}));
|
|
13821
14348
|
program.command("settings").description("Show a space's guest and visitor switches, or flip them (space admins; the same switches as in the app)").option("--space <id>", "space id or link (default: the first target in .cloudtifact.json)").option(SETTINGS[0].option, SETTINGS[0].label).option(SETTINGS[1].option, `${SETTINGS[1].label} (off keeps the database private from visitors)`).option(SETTINGS[2].option, SETTINGS[2].label).option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
|
|
13822
14349
|
const { spaceId } = await pickSpace(opts.space);
|
|
@@ -13827,6 +14354,187 @@ program.command("settings").description("Show a space's guest and visitor switch
|
|
|
13827
14354
|
if (format !== "text") return printData(format, settingsData(space));
|
|
13828
14355
|
process.stdout.write(settingsText(space));
|
|
13829
14356
|
}));
|
|
14357
|
+
for (const [name, on] of [["archive", true], ["unarchive", false]]) program.command(name).description(on ? "Archive a space: it leaves every list, its link, comments and data keep working, and a new version brings it back" : "Bring an archived space back into the lists").argument("[space]", "space id or link (default: the first target in .cloudtifact.json)").option("--space <id>", "space id or link (same as the argument)").option("--json", "print JSON").action(run(async (arg, opts) => {
|
|
14358
|
+
const { spaceId } = await pickSpace(arg ?? opts.space);
|
|
14359
|
+
const { client } = await getClient();
|
|
14360
|
+
const space = on ? await client.archiveSpace(spaceId) : await client.unarchiveSpace(spaceId);
|
|
14361
|
+
if (opts.json) return printData("json", {
|
|
14362
|
+
id: space.id,
|
|
14363
|
+
name: space.name,
|
|
14364
|
+
archived: on,
|
|
14365
|
+
link: space.url
|
|
14366
|
+
});
|
|
14367
|
+
console.log(on ? `Archived "${space.name}". It's out of the lists; the link still works: ${space.url}\nUndo: cloudtifact unarchive ${space.id}` : `"${space.name}" is back in the lists: ${space.url}`);
|
|
14368
|
+
}));
|
|
14369
|
+
program.command("spaces").description("List spaces: the ones you made, the ones shared with you, or the ones waiting on you").option("--mine", "only the ones you made (Made by me)").option("--shared", "only the ones people in other workspaces shared with you (Shared with me)").option("--waiting", "comment threads waiting on you, by space (Waiting on me)").option("--workspace <slug>", "which workspace (default: your default one)").option("--folder <path>", "only the spaces in this folder (\"/\" for the ones in no folder)").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
|
|
14370
|
+
const { client, config } = await getClient();
|
|
14371
|
+
const me = await client.me();
|
|
14372
|
+
const format = resolveFormat(opts);
|
|
14373
|
+
const mineSlugs = new Set(me.workspaces.map((w) => w.slug));
|
|
14374
|
+
if (opts.shared) {
|
|
14375
|
+
const rows = (await client.listOpenableSpaces()).liveSpaces.filter((s) => !mineSlugs.has(s.workspace.slug)).map((s) => ({
|
|
14376
|
+
id: s.id,
|
|
14377
|
+
name: s.name,
|
|
14378
|
+
workspace: s.workspace.slug,
|
|
14379
|
+
link: s.url
|
|
14380
|
+
}));
|
|
14381
|
+
if (format !== "text") return printData(format, { spaces: rows });
|
|
14382
|
+
if (!rows.length) return console.log("Nobody has shared a space with you from another workspace.");
|
|
14383
|
+
for (const r of rows) console.log(`${r.id} ${r.name} · ${r.workspace}`);
|
|
14384
|
+
return;
|
|
14385
|
+
}
|
|
14386
|
+
const slug = requireWorkspace(me.workspaces, {
|
|
14387
|
+
flag: opts.workspace,
|
|
14388
|
+
saved: config.workspace
|
|
14389
|
+
}).workspace.slug;
|
|
14390
|
+
if (opts.waiting) {
|
|
14391
|
+
const w = await client.waiting(slug, { limit: 50 });
|
|
14392
|
+
const rows = w.threads.map((t) => ({
|
|
14393
|
+
space: t.space.id,
|
|
14394
|
+
name: t.space.name,
|
|
14395
|
+
thread: t.thread.id,
|
|
14396
|
+
why: t.mentioned ? "mentioned you" : t.reason === "owner" ? "on a space you started" : "you wrote in it",
|
|
14397
|
+
at: iso(t.lastAt)
|
|
14398
|
+
}));
|
|
14399
|
+
if (format !== "text") return printData(format, {
|
|
14400
|
+
workspace: slug,
|
|
14401
|
+
total: w.total,
|
|
14402
|
+
threads: rows
|
|
14403
|
+
});
|
|
14404
|
+
if (!rows.length) return console.log("Nothing is waiting on you.");
|
|
14405
|
+
for (const r of rows) console.log(`${r.space} ${r.name} · ${r.why} · ${r.at} UTC`);
|
|
14406
|
+
console.log(`Read one: cloudtifact comments --space <id>`);
|
|
14407
|
+
return;
|
|
14408
|
+
}
|
|
14409
|
+
const [{ spaces }, { folders }] = await Promise.all([client.listSpaces({ workspace: slug }), client.listFolders(slug).catch(() => ({ folders: [] }))]);
|
|
14410
|
+
const only = opts.folder === void 0 ? void 0 : (await folderFor(client, slug, opts.folder))?.id ?? null;
|
|
14411
|
+
const pathOf = new Map(folders.map((f) => [f.id, f.path]));
|
|
14412
|
+
const rows = spaces.filter((s) => s.kind !== "app" && (!opts.mine || s.createdBy === me.user.id)).filter((s) => only === void 0 || (s.folderId ?? null) === only).map((s) => ({
|
|
14413
|
+
id: s.id,
|
|
14414
|
+
name: s.name,
|
|
14415
|
+
folder: s.folderId ? pathOf.get(s.folderId) ?? null : null,
|
|
14416
|
+
project: s.project ?? null,
|
|
14417
|
+
visibility: s.visibility,
|
|
14418
|
+
link: s.shareUrl ?? s.url
|
|
14419
|
+
}));
|
|
14420
|
+
if (format !== "text") return printData(format, {
|
|
14421
|
+
workspace: slug,
|
|
14422
|
+
spaces: rows
|
|
14423
|
+
});
|
|
14424
|
+
if (!rows.length) return console.log(opts.mine ? "You haven't made a space here yet." : "No spaces yet.");
|
|
14425
|
+
for (const r of rows) console.log(`${r.id} ${r.name}${r.folder ? ` ▤ ${r.folder}` : ""}${r.project ? ` · ${r.project}` : ""}`);
|
|
14426
|
+
}));
|
|
14427
|
+
program.command("activity").description("What people did to the spaces of a workspace, newest first, with the id to undo each").option("--workspace <slug>", "which workspace (default: your default one)").option("--person <handle>", "only what this person did").option("-n, --limit <n>", "how many to show", "20").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
|
|
14428
|
+
const { client, config } = await getClient();
|
|
14429
|
+
const cur = requireWorkspace((await client.me()).workspaces, {
|
|
14430
|
+
flag: opts.workspace,
|
|
14431
|
+
saved: config.workspace
|
|
14432
|
+
});
|
|
14433
|
+
const { events } = await client.listActivity(cur.workspace.slug, {
|
|
14434
|
+
person: opts.person?.replace(/^@/, ""),
|
|
14435
|
+
limit: Math.min(100, Math.max(1, Number(opts.limit) || 20))
|
|
14436
|
+
});
|
|
14437
|
+
const rows = events.map((e) => ({
|
|
14438
|
+
id: e.id,
|
|
14439
|
+
at: iso(e.createdAt),
|
|
14440
|
+
what: `${byPerson(e.actor ? e.actor.name ?? `@${e.actor.handle}` : "A deleted account", e.via)} ${describeActivity(e)}`,
|
|
14441
|
+
space: e.space?.id ?? null,
|
|
14442
|
+
folder: e.folder?.id ?? null,
|
|
14443
|
+
canUndo: e.canUndo,
|
|
14444
|
+
undone: e.undone
|
|
14445
|
+
}));
|
|
14446
|
+
const format = resolveFormat(opts);
|
|
14447
|
+
if (format !== "text") return printData(format, {
|
|
14448
|
+
workspace: cur.workspace.slug,
|
|
14449
|
+
events: rows
|
|
14450
|
+
});
|
|
14451
|
+
if (!rows.length) return console.log("Nothing yet.");
|
|
14452
|
+
for (const r of rows) console.log(`${r.id} ${r.at} UTC ${r.what}${r.undone ? " (undone)" : r.canUndo ? "" : " (can't undo)"}`);
|
|
14453
|
+
if (rows.some((r) => r.canUndo)) console.log("Undo one: cloudtifact undo <id>");
|
|
14454
|
+
}));
|
|
14455
|
+
program.command("undo").description("Undo a change from `cloudtifact activity`: brings back, restores, or takes out an added page").argument("<id>", "the change's id, from `cloudtifact activity`").option("--workspace <slug>", "which workspace (default: your default one)").action(run(async (id, opts) => {
|
|
14456
|
+
const { client, config } = await getClient();
|
|
14457
|
+
const cur = requireWorkspace((await client.me()).workspaces, {
|
|
14458
|
+
flag: opts.workspace,
|
|
14459
|
+
saved: config.workspace
|
|
14460
|
+
});
|
|
14461
|
+
const { event } = await client.undoActivity(cur.workspace.slug, id);
|
|
14462
|
+
console.log(`Undone: ${byPerson(event.actor ? event.actor.name ?? `@${event.actor.handle}` : "You", event.via)} ${describeActivity(event)}`);
|
|
14463
|
+
}));
|
|
14464
|
+
/** The workspace a folder command works in. */
|
|
14465
|
+
async function folderWorkspace(opts) {
|
|
14466
|
+
const { client, config } = await getClient();
|
|
14467
|
+
return {
|
|
14468
|
+
client,
|
|
14469
|
+
slug: requireWorkspace((await client.me()).workspaces, {
|
|
14470
|
+
flag: opts.workspace,
|
|
14471
|
+
saved: config.workspace
|
|
14472
|
+
}).workspace.slug
|
|
14473
|
+
};
|
|
14474
|
+
}
|
|
14475
|
+
program.command("folders").description("The workspace's folders: every path, how many spaces, and what each is for (read it before filing)").option("--workspace <slug>", "which workspace (default: your default one)").option("--toon", "print TOON (compact, for agents)").option("--json", "print JSON").action(run(async (opts) => {
|
|
14476
|
+
const { client, slug } = await folderWorkspace(opts);
|
|
14477
|
+
const { folders } = await client.listFolders(slug);
|
|
14478
|
+
const format = resolveFormat(opts);
|
|
14479
|
+
if (format !== "text") return printData(format, {
|
|
14480
|
+
workspace: slug,
|
|
14481
|
+
folders: folders.map((f) => ({
|
|
14482
|
+
id: f.id,
|
|
14483
|
+
path: f.path,
|
|
14484
|
+
spaces: f.spaces,
|
|
14485
|
+
description: f.description
|
|
14486
|
+
}))
|
|
14487
|
+
});
|
|
14488
|
+
if (!folders.length) return console.log("No folders yet. Make one: cloudtifact folder create \"Product plans\"");
|
|
14489
|
+
for (const l of treeLines(folders)) console.log(l);
|
|
14490
|
+
}));
|
|
14491
|
+
const folder = program.command("folder").description("Make, rename, describe or move folders (each change is in Activity, with Undo)");
|
|
14492
|
+
folder.command("create").description("Make a folder; a path like \"Deals/Enterprise\" makes the folders it needs").argument("<path>", "the folder's path").option("--description <text>", "what goes in it (people and agents read this before filing)").option("--workspace <slug>", "which workspace (default: your default one)").action(run(async (p, opts) => {
|
|
14493
|
+
const { client, slug } = await folderWorkspace(opts);
|
|
14494
|
+
const { folder: f, made } = await ensureFolderPath(client, slug, p, opts.description);
|
|
14495
|
+
if (!made.length) {
|
|
14496
|
+
console.log(`"${f.path}" is already there (${f.id}).`);
|
|
14497
|
+
if (opts.description) console.error(`note: its description wasn't changed. To change it: cloudtifact folder describe "${f.path}" "…"`);
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
14500
|
+
console.log(`Made ${made.map((m) => `"${m.path}"`).join(", ")} (${f.id}).`);
|
|
14501
|
+
}));
|
|
14502
|
+
folder.command("rename").description("Rename a folder").argument("<path>", "the folder's path or id").argument("<name>", "its new name").option("--workspace <slug>", "which workspace (default: your default one)").action(run(async (p, name, opts) => {
|
|
14503
|
+
const { client, slug } = await folderWorkspace(opts);
|
|
14504
|
+
const f = await folderFor(client, slug, p);
|
|
14505
|
+
if (!f) throw new CliError("The top level can't be renamed.");
|
|
14506
|
+
const next = await client.updateFolder(slug, f.id, { name });
|
|
14507
|
+
console.log(`Renamed "${f.path}" to "${next.path}".`);
|
|
14508
|
+
}));
|
|
14509
|
+
folder.command("describe").description("Say what goes in a folder (people and agents read it before filing); empty text clears it").argument("<path>", "the folder's path or id").argument("<text>", "one line").option("--workspace <slug>", "which workspace (default: your default one)").action(run(async (p, text, opts) => {
|
|
14510
|
+
const { client, slug } = await folderWorkspace(opts);
|
|
14511
|
+
const f = await folderFor(client, slug, p);
|
|
14512
|
+
if (!f) throw new CliError("The top level has no description.");
|
|
14513
|
+
await client.updateFolder(slug, f.id, { description: text.trim() || null });
|
|
14514
|
+
console.log(text.trim() ? `Described "${f.path}".` : `Cleared the description of "${f.path}".`);
|
|
14515
|
+
}));
|
|
14516
|
+
folder.command("move").description("Move a folder (and everything in it) into another one, or to the top level with \"/\"").argument("<path>", "the folder's path or id").argument("<to>", "where: a folder path, or \"/\" for the top level").option("--workspace <slug>", "which workspace (default: your default one)").action(run(async (p, to, opts) => {
|
|
14517
|
+
const { client, slug } = await folderWorkspace(opts);
|
|
14518
|
+
const f = await folderFor(client, slug, p);
|
|
14519
|
+
if (!f) throw new CliError("The top level can't move.");
|
|
14520
|
+
const into = await folderFor(client, slug, to);
|
|
14521
|
+
const next = await client.updateFolder(slug, f.id, { parentId: into?.id ?? null });
|
|
14522
|
+
console.log(`Moved "${f.name}" to ${shownPath(into)}: now "${next.path}".`);
|
|
14523
|
+
}));
|
|
14524
|
+
program.command("move").description("Move a space into a folder, or to the top level with \"/\"; its link and who can open it stay the same").argument("<space>", "space id or link").argument("<folder>", "a folder path like \"Product plans/Pricing\", or \"/\"").action(run(async (ref, to) => {
|
|
14525
|
+
const { client } = await getClient();
|
|
14526
|
+
const space = await client.getSpace(spaceIdOrThrow(ref));
|
|
14527
|
+
const into = await folderFor(client, space.workspace.slug, to);
|
|
14528
|
+
await client.fileSpace(space.id, into?.id ?? null);
|
|
14529
|
+
console.log(`Moved "${space.name}" to ${shownPath(into)}. Undo: cloudtifact activity`);
|
|
14530
|
+
}));
|
|
14531
|
+
program.command("favorite").description("Add a space to your favorites, at the top of your sidebar (only you see them)").argument("[space]", "space id or link (default: the first target in .cloudtifact.json)").option("--space <id>", "space id or link (same as the argument)").option("--remove", "take it off your favorites").action(run(async (arg, opts) => {
|
|
14532
|
+
const { spaceId } = await pickSpace(arg ?? opts.space);
|
|
14533
|
+
const { client } = await getClient();
|
|
14534
|
+
if (opts.remove) await client.unfavoriteSpace(spaceId);
|
|
14535
|
+
else await client.favoriteSpace(spaceId);
|
|
14536
|
+
console.log(opts.remove ? `Removed ${spaceId} from your favorites.` : `Added ${spaceId} to your favorites.`);
|
|
14537
|
+
}));
|
|
13830
14538
|
program.command("upload").description("Upload files of any type into a space (up to 100 MB each); download them anywhere you sign in").argument("<files...>").option("--space <id>", "space id or link (default: the first target in .cloudtifact.json)").action(run(async (files, opts) => {
|
|
13831
14539
|
const { spaceId } = await pickSpace(opts.space, { noRepo: "Pass --space <id>, or run inside a linked repo." });
|
|
13832
14540
|
const { client } = await getClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudtifact",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Publish pages and docs to Cloudtifact as links people comment on, with every version kept and the comments handed back to your agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "The Cloudtifact authors",
|