@craftspace/cli 0.3.0 → 0.4.1
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 +90 -168
- package/dist/machine.d.ts +2 -5
- package/dist/machine.js +53 -129
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -18969,54 +18969,54 @@ var AUDIT_GROUPS = [
|
|
|
18969
18969
|
];
|
|
18970
18970
|
var AUDIT_LISTED_ACTIONS = AUDIT_GROUPS.flatMap((group) => group.actions);
|
|
18971
18971
|
var AUDIT_DEFAULT_ACTIONS = AUDIT_LISTED_ACTIONS.filter((action) => AUDIT_TIERS[action] !== AuditTier.Volume);
|
|
18972
|
-
var
|
|
18973
|
-
[AuditAction.AccessDenied]: "
|
|
18972
|
+
var AUDIT_LABELS = {
|
|
18973
|
+
[AuditAction.AccessDenied]: "Denied request",
|
|
18974
18974
|
[AuditAction.MemberSignedIn]: "Signed in",
|
|
18975
|
-
[AuditAction.MemberInvited]: "Invited",
|
|
18975
|
+
[AuditAction.MemberInvited]: "Invited member",
|
|
18976
18976
|
[AuditAction.MemberInviteAccepted]: "Joined the organization",
|
|
18977
|
-
[AuditAction.MemberRoleChanged]: "Changed
|
|
18978
|
-
[AuditAction.MemberRemoved]: "Removed",
|
|
18977
|
+
[AuditAction.MemberRoleChanged]: "Changed member role",
|
|
18978
|
+
[AuditAction.MemberRemoved]: "Removed member",
|
|
18979
18979
|
[AuditAction.TokenMinted]: "Minted member token",
|
|
18980
18980
|
[AuditAction.TokenRevoked]: "Revoked member token",
|
|
18981
|
-
[AuditAction.AdminEnteredOrg]: "Opened support view
|
|
18982
|
-
[AuditAction.OrganizationRenamed]: "Renamed the organization
|
|
18983
|
-
[AuditAction.OrganizationWebsiteChanged]: "
|
|
18984
|
-
[AuditAction.OrganizationModelChanged]: "
|
|
18981
|
+
[AuditAction.AdminEnteredOrg]: "Opened support view",
|
|
18982
|
+
[AuditAction.OrganizationRenamed]: "Renamed the organization",
|
|
18983
|
+
[AuditAction.OrganizationWebsiteChanged]: "Changed the organization website",
|
|
18984
|
+
[AuditAction.OrganizationModelChanged]: "Changed the default model",
|
|
18985
18985
|
[AuditAction.OrganizationDeleted]: "Deleted the organization",
|
|
18986
|
-
[AuditAction.ConnectionCreated]: "Connected",
|
|
18987
|
-
[AuditAction.ConnectionReconnected]: "Reconnected",
|
|
18988
|
-
[AuditAction.ConnectionDeleted]: "Disconnected",
|
|
18989
|
-
[AuditAction.ConnectionAccessChanged]: "Changed who can use",
|
|
18986
|
+
[AuditAction.ConnectionCreated]: "Connected app",
|
|
18987
|
+
[AuditAction.ConnectionReconnected]: "Reconnected app",
|
|
18988
|
+
[AuditAction.ConnectionDeleted]: "Disconnected app",
|
|
18989
|
+
[AuditAction.ConnectionAccessChanged]: "Changed who can use app",
|
|
18990
18990
|
[AuditAction.McpConnectionCreated]: "Attached MCP server",
|
|
18991
18991
|
[AuditAction.McpConnectionDeleted]: "Detached MCP server",
|
|
18992
|
-
[AuditAction.McpConnectionToolsChanged]: "Changed
|
|
18993
|
-
[AuditAction.PageCreated]: "Created",
|
|
18994
|
-
[AuditAction.PageUpdated]: "
|
|
18995
|
-
[AuditAction.PageDeleted]: "Deleted",
|
|
18996
|
-
[AuditAction.PageMoved]: "Moved",
|
|
18997
|
-
[AuditAction.PageShared]: "Published a public link for",
|
|
18998
|
-
[AuditAction.PageUnshared]: "Revoked the public link for",
|
|
18992
|
+
[AuditAction.McpConnectionToolsChanged]: "Changed MCP server tools",
|
|
18993
|
+
[AuditAction.PageCreated]: "Created page",
|
|
18994
|
+
[AuditAction.PageUpdated]: "Updated page",
|
|
18995
|
+
[AuditAction.PageDeleted]: "Deleted page",
|
|
18996
|
+
[AuditAction.PageMoved]: "Moved page",
|
|
18997
|
+
[AuditAction.PageShared]: "Published a public link for page",
|
|
18998
|
+
[AuditAction.PageUnshared]: "Revoked the public link for page",
|
|
18999
18999
|
[AuditAction.SpaceCreated]: "Created space",
|
|
19000
|
-
[AuditAction.SpaceRenamed]: "Renamed space
|
|
19000
|
+
[AuditAction.SpaceRenamed]: "Renamed space",
|
|
19001
19001
|
[AuditAction.SpaceDeleted]: "Deleted space",
|
|
19002
19002
|
[AuditAction.SpaceAccessChanged]: "Changed who can reach space",
|
|
19003
19003
|
[AuditAction.TableCreated]: "Created table",
|
|
19004
19004
|
[AuditAction.TableDeleted]: "Deleted table",
|
|
19005
19005
|
[AuditAction.AgentCreated]: "Created agent",
|
|
19006
19006
|
[AuditAction.AgentDeleted]: "Deleted agent",
|
|
19007
|
-
[AuditAction.AgentGrantsChanged]: "Changed what
|
|
19008
|
-
[AuditAction.ChannelConnected]: "Connected",
|
|
19009
|
-
[AuditAction.ChannelDisconnected]: "Disconnected",
|
|
19007
|
+
[AuditAction.AgentGrantsChanged]: "Changed what agent can reach",
|
|
19008
|
+
[AuditAction.ChannelConnected]: "Connected channel",
|
|
19009
|
+
[AuditAction.ChannelDisconnected]: "Disconnected channel",
|
|
19010
19010
|
[AuditAction.KitInstalled]: "Installed kit",
|
|
19011
19011
|
[AuditAction.KitRemoved]: "Removed kit",
|
|
19012
19012
|
[AuditAction.WorkflowCreated]: "Created workflow",
|
|
19013
19013
|
[AuditAction.WorkflowPublished]: "Published workflow",
|
|
19014
19014
|
[AuditAction.WorkflowDeleted]: "Deleted workflow",
|
|
19015
|
-
[AuditAction.WorkflowSecretRotated]: "Rotated
|
|
19016
|
-
[AuditAction.McpRequest]: "Called
|
|
19017
|
-
[AuditAction.McpToolCall]: "Called tool",
|
|
19018
|
-
[AuditAction.PageRead]: "Read",
|
|
19019
|
-
[AuditAction.PageWrite]: "Wrote"
|
|
19015
|
+
[AuditAction.WorkflowSecretRotated]: "Rotated workflow webhook secret",
|
|
19016
|
+
[AuditAction.McpRequest]: "Called MCP method",
|
|
19017
|
+
[AuditAction.McpToolCall]: "Called MCP tool",
|
|
19018
|
+
[AuditAction.PageRead]: "Read page",
|
|
19019
|
+
[AuditAction.PageWrite]: "Wrote page"
|
|
19020
19020
|
};
|
|
19021
19021
|
var AuditTargetSchema = external_exports.object({
|
|
19022
19022
|
type: external_exports.string(),
|
|
@@ -19782,7 +19782,7 @@ var SaveWorkstationBodySchema = external_exports.object({
|
|
|
19782
19782
|
var WorkstationResponseSchema = external_exports.object({ workstation: WorkstationSchema });
|
|
19783
19783
|
|
|
19784
19784
|
// ../../shared/dist/machines/machine.js
|
|
19785
|
-
var CLI_VERSION = "0.
|
|
19785
|
+
var CLI_VERSION = "0.4.1";
|
|
19786
19786
|
var CLI_PACKAGE = "@craftspace/cli";
|
|
19787
19787
|
var CLI_INSTALL = `npm i -g ${CLI_PACKAGE}`;
|
|
19788
19788
|
var MACHINE_BEAT_INTERVAL_MS = 15e3;
|
|
@@ -20250,7 +20250,7 @@ var MemberActivityListResponse = external_exports.object({
|
|
|
20250
20250
|
});
|
|
20251
20251
|
|
|
20252
20252
|
// dist/machine.js
|
|
20253
|
-
import { execFile as execFile4 } from "node:child_process";
|
|
20253
|
+
import { execFile as execFile4, spawn as spawn2 } from "node:child_process";
|
|
20254
20254
|
import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "node:fs/promises";
|
|
20255
20255
|
import os5 from "node:os";
|
|
20256
20256
|
import path4 from "node:path";
|
|
@@ -23112,10 +23112,13 @@ var machine = {
|
|
|
23112
23112
|
};
|
|
23113
23113
|
return answer.machine;
|
|
23114
23114
|
},
|
|
23115
|
+
async url() {
|
|
23116
|
+
return (await readConfig())?.url ?? null;
|
|
23117
|
+
},
|
|
23115
23118
|
async signIn({ url: url2, why, install = false }) {
|
|
23116
23119
|
const where = url2 ?? (await readConfig())?.url ?? DEFAULT_URL;
|
|
23117
23120
|
if (!process.stdin.isTTY)
|
|
23118
|
-
throw new Error(`${why} Run:
|
|
23121
|
+
throw new Error(`${why} Run: cs login --token <token>`);
|
|
23119
23122
|
const page = `${where}/workstations`;
|
|
23120
23123
|
ui.say(`
|
|
23121
23124
|
${ui.attention(why)}
|
|
@@ -23193,21 +23196,12 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23193
23196
|
await new Promise((resolve) => setTimeout(resolve, nextDelayMs(failures, busy)));
|
|
23194
23197
|
}
|
|
23195
23198
|
},
|
|
23196
|
-
async
|
|
23197
|
-
|
|
23198
|
-
const
|
|
23199
|
-
if (
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
}
|
|
23203
|
-
ui.say(`${ui.clear()}${ui.waiting(`installing ${moved}`)}`);
|
|
23204
|
-
const done = await update.toLatest();
|
|
23205
|
-
ui.say(`${ui.clear()}${done === null ? ui.bad(`could not install ${moved}`, `run it yourself: ${CLI_INSTALL}@latest`) : ui.ok(`craftspace ${ui.name(CLI_VERSION)} -> ${ui.name(done)}`)}`);
|
|
23206
|
-
},
|
|
23207
|
-
async run({ argv, on }) {
|
|
23208
|
-
if (on === void 0)
|
|
23209
|
-
return runHere(argv);
|
|
23210
|
-
return runThere({ argv, on });
|
|
23199
|
+
async run() {
|
|
23200
|
+
const first = await signedInOrAsk();
|
|
23201
|
+
const { auth, value: answer } = await withAuth(first, (live) => call({ ...live, method: "GET", path: "/api/machines", schema: MachinesResponseSchema }));
|
|
23202
|
+
if (answer.machines.length === 0)
|
|
23203
|
+
return nowhereToGo(auth);
|
|
23204
|
+
return openTerminal(await pick2(answer.machines));
|
|
23211
23205
|
},
|
|
23212
23206
|
async daemon() {
|
|
23213
23207
|
if (process.env.CRAFTSPACE_NO_SERVICE === "1")
|
|
@@ -23235,122 +23229,61 @@ async function requireSignedIn() {
|
|
|
23235
23229
|
const config2 = await readConfig();
|
|
23236
23230
|
const token = await readToken();
|
|
23237
23231
|
if (!config2 || !token)
|
|
23238
|
-
throw new Error("This machine is not signed in. Run:
|
|
23232
|
+
throw new Error("This machine is not signed in. Run: cs login --token <token>");
|
|
23239
23233
|
return { url: config2.url, token };
|
|
23240
23234
|
}
|
|
23241
|
-
async function
|
|
23242
|
-
|
|
23243
|
-
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
async function runThere({ argv, on }) {
|
|
23251
|
-
const first = await signedInOrAsk();
|
|
23252
|
-
const { auth, value: target } = await withAuth(first, (live) => machineNamed({ ...live, name: on }));
|
|
23253
|
-
const { url: url2, token } = auth;
|
|
23254
|
-
let run5 = await call({
|
|
23255
|
-
url: url2,
|
|
23256
|
-
token,
|
|
23257
|
-
method: "POST",
|
|
23258
|
-
path: `/api/machines/${target.id}/runs`,
|
|
23259
|
-
body: { argv },
|
|
23260
|
-
schema: MachineRunSchema
|
|
23235
|
+
async function pick2(machines) {
|
|
23236
|
+
return dist_default6({
|
|
23237
|
+
message: "Which workstation?",
|
|
23238
|
+
choices: machines.map((candidate) => ({
|
|
23239
|
+
name: `${candidate.name.padEnd(24)} ${candidate.state.padEnd(12)} ${specs(candidate)} ${beat(candidate)}`,
|
|
23240
|
+
value: candidate,
|
|
23241
|
+
description: candidate.sshAddress === null ? "No address yet, so nothing to open" : void 0
|
|
23242
|
+
})),
|
|
23243
|
+
pageSize: 12
|
|
23261
23244
|
});
|
|
23262
|
-
ui.say(`${ui.name(target.name)} ${ui.dim(`\xB7 queued ${run5.id}`)}
|
|
23263
|
-
|
|
23264
|
-
`);
|
|
23265
|
-
let printed = 0;
|
|
23266
|
-
for (; ; ) {
|
|
23267
|
-
if (run5.output.length > printed) {
|
|
23268
|
-
process.stdout.write(run5.output.slice(printed));
|
|
23269
|
-
printed = run5.output.length;
|
|
23270
|
-
}
|
|
23271
|
-
if (run5.state === "done")
|
|
23272
|
-
return 0;
|
|
23273
|
-
if (run5.state === "failed")
|
|
23274
|
-
return run5.exitCode ?? 1;
|
|
23275
|
-
if (run5.state === "needs_input") {
|
|
23276
|
-
ui.say(`
|
|
23277
|
-
${ui.attention(`${target.name} is asking:`)} ${run5.question ?? "it needs a decision"}
|
|
23278
|
-
> `);
|
|
23279
|
-
const reply = await readStdinLine();
|
|
23280
|
-
if (reply === null)
|
|
23281
|
-
return 130;
|
|
23282
|
-
run5 = await call({
|
|
23283
|
-
url: url2,
|
|
23284
|
-
token,
|
|
23285
|
-
method: "POST",
|
|
23286
|
-
path: `/api/machines/runs/${run5.id}/answer`,
|
|
23287
|
-
body: { text: reply },
|
|
23288
|
-
schema: MachineRunSchema
|
|
23289
|
-
});
|
|
23290
|
-
printed = run5.output.length;
|
|
23291
|
-
continue;
|
|
23292
|
-
}
|
|
23293
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
23294
|
-
run5 = await readRun({ url: url2, token, machineId: target.id, runId: run5.id });
|
|
23295
|
-
}
|
|
23296
23245
|
}
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
url: url2,
|
|
23300
|
-
token,
|
|
23301
|
-
method: "GET",
|
|
23302
|
-
path: `/api/machines/${machineId}/runs`,
|
|
23303
|
-
schema: MachineRunsResponseSchema
|
|
23304
|
-
});
|
|
23305
|
-
const found = answer.runs.find((candidate) => candidate.id === runId);
|
|
23306
|
-
if (found === void 0)
|
|
23307
|
-
throw new Error(`Run ${runId} is gone.`);
|
|
23308
|
-
return found;
|
|
23246
|
+
function specs({ platform, cores, memoryGb }) {
|
|
23247
|
+
return [platform, cores === null ? null : `${cores} vCPU`, memoryGb === null ? null : `${memoryGb} GB`].filter((part) => part !== null).join(" \xB7 ");
|
|
23309
23248
|
}
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23249
|
+
function beat({ lastBeatAt }) {
|
|
23250
|
+
if (lastBeatAt === null)
|
|
23251
|
+
return "never beat";
|
|
23252
|
+
const seconds = Math.max(0, Math.round((Date.now() - new Date(lastBeatAt).getTime()) / 1e3));
|
|
23253
|
+
if (seconds < 60)
|
|
23254
|
+
return `beat ${seconds}s ago`;
|
|
23255
|
+
if (seconds < 3600)
|
|
23256
|
+
return `beat ${Math.round(seconds / 60)}m ago`;
|
|
23257
|
+
return `beat ${Math.round(seconds / 3600)}h ago`;
|
|
23258
|
+
}
|
|
23259
|
+
async function openTerminal(target) {
|
|
23260
|
+
if (target.sshAddress === null) {
|
|
23261
|
+
throw new Error(`${target.name} has not told us an address yet. It reports one on its next beat.`);
|
|
23319
23262
|
}
|
|
23263
|
+
const where = `${target.sshUser ?? "root"}@${target.sshAddress}`;
|
|
23320
23264
|
ui.say(`
|
|
23321
|
-
${ui.
|
|
23265
|
+
${ui.dim("\u2192")} ${ui.name(target.name)} ${ui.dim(`\xB7 ${where}`)}
|
|
23322
23266
|
|
|
23323
23267
|
`);
|
|
23324
|
-
return dist_default6({
|
|
23325
|
-
message: "Run it on",
|
|
23326
|
-
choices: answer.machines.map((candidate) => ({
|
|
23327
|
-
name: `${candidate.name.padEnd(20)} ${candidate.state}`,
|
|
23328
|
-
value: candidate
|
|
23329
|
-
}))
|
|
23330
|
-
});
|
|
23331
|
-
}
|
|
23332
|
-
function readStdinLine() {
|
|
23333
23268
|
return new Promise((resolve) => {
|
|
23334
|
-
|
|
23335
|
-
|
|
23336
|
-
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
|
|
23340
|
-
resolve(line.trim() === "" ? null : line.trim());
|
|
23341
|
-
};
|
|
23342
|
-
const onData = (chunk) => {
|
|
23343
|
-
buffer += chunk;
|
|
23344
|
-
const at = buffer.indexOf("\n");
|
|
23345
|
-
if (at !== -1)
|
|
23346
|
-
done(buffer.slice(0, at));
|
|
23347
|
-
};
|
|
23348
|
-
const onEnd = () => done(buffer);
|
|
23349
|
-
process.stdin.on("data", onData);
|
|
23350
|
-
process.stdin.on("end", onEnd);
|
|
23351
|
-
process.stdin.resume();
|
|
23269
|
+
const session = spawn2("ssh", ["-t", where], { stdio: "inherit" });
|
|
23270
|
+
session.on("error", (error51) => {
|
|
23271
|
+
ui.say(ui.bad(`could not start ssh`, error51.message));
|
|
23272
|
+
resolve(1);
|
|
23273
|
+
});
|
|
23274
|
+
session.on("exit", (code) => resolve(code ?? 0));
|
|
23352
23275
|
});
|
|
23353
23276
|
}
|
|
23277
|
+
function nowhereToGo({ url: url2 }) {
|
|
23278
|
+
ui.say(`
|
|
23279
|
+
${ui.attention("No workstations here yet.")}
|
|
23280
|
+
|
|
23281
|
+
`);
|
|
23282
|
+
ui.say(ui.field("Add one", ui.link(`${url2}/workstations`)));
|
|
23283
|
+
ui.say(ui.field("Or sign in", "a Mac or Linux box with: cs login"));
|
|
23284
|
+
ui.say("\n");
|
|
23285
|
+
return 1;
|
|
23286
|
+
}
|
|
23354
23287
|
async function tools() {
|
|
23355
23288
|
if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
|
|
23356
23289
|
return probed.tools;
|
|
@@ -23475,14 +23408,6 @@ ${KEYS_END}
|
|
|
23475
23408
|
function trimEnd(text) {
|
|
23476
23409
|
return text.replace(/\s+$/, "");
|
|
23477
23410
|
}
|
|
23478
|
-
async function writeSession(session) {
|
|
23479
|
-
const dir = path4.join(machine.home(), "sessions");
|
|
23480
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
23481
|
-
await writeFile(path4.join(dir, `${session.id}.json`), JSON.stringify(session), { mode: 384 });
|
|
23482
|
-
}
|
|
23483
|
-
async function clearSession(id) {
|
|
23484
|
-
await rm(path4.join(machine.home(), "sessions", `${id}.json`), { force: true });
|
|
23485
|
-
}
|
|
23486
23411
|
async function writeAgentConfigs({ url: url2, token, writes }) {
|
|
23487
23412
|
const planned = writes.length > 0 ? writes : defaultWrites({ url: url2, token });
|
|
23488
23413
|
const owned = [];
|
|
@@ -23676,7 +23601,6 @@ var MAX_REPORTED_SESSIONS = 50;
|
|
|
23676
23601
|
var MAX_BACKOFF_MS = 12e4;
|
|
23677
23602
|
var KEYS_BEGIN = "# craftspace begin";
|
|
23678
23603
|
var KEYS_END = "# craftspace end";
|
|
23679
|
-
var POLL_INTERVAL_MS = 1e3;
|
|
23680
23604
|
var UPDATE_EVERY_MS = 6 * 60 * 60 * 1e3;
|
|
23681
23605
|
var BEATING = "beating every 15s, outbound only, and keeps this set up";
|
|
23682
23606
|
var LINGER_LINE = `sudo loginctl enable-linger ${os5.userInfo().username}`;
|
|
@@ -23689,9 +23613,9 @@ var running = /* @__PURE__ */ new Set();
|
|
|
23689
23613
|
// dist/index.js
|
|
23690
23614
|
setup.widenPath();
|
|
23691
23615
|
var program2 = new Command();
|
|
23692
|
-
program2.enablePositionalOptions().name("
|
|
23693
|
-
program2.command("login").description("sign this machine in").option("--token <token>", "the login token from your team", process.env.CRAFTSPACE_TOKEN).option("--url <url>", "your control plane
|
|
23694
|
-
const where = url2.replace(/\/$/, "");
|
|
23616
|
+
program2.enablePositionalOptions().name("cs").description("Sign this machine in to your team, so every agent on it reads the same brain.").version(CLI_VERSION).showHelpAfterError();
|
|
23617
|
+
program2.command("login").description("sign this machine in").option("--token <token>", "the login token from your team", process.env.CRAFTSPACE_TOKEN).option("--url <url>", "your control plane, remembered after the first login").option("--no-install", "do not install anything this machine is missing").action(async ({ token, url: url2, install }) => {
|
|
23618
|
+
const where = (url2 ?? process.env.CRAFTSPACE_URL ?? await machine.url() ?? DEFAULT_URL2).replace(/\/$/, "");
|
|
23695
23619
|
const signed = token === void 0 ? (await machine.signIn({ url: where, why: "Signing this machine in.", install })).machine : await machine.login({ token, url: where, install });
|
|
23696
23620
|
const daemon = await machine.daemon();
|
|
23697
23621
|
ui.say(ui.heading(`Signed in as ${signed.ownerName}`));
|
|
@@ -23702,8 +23626,8 @@ program2.command("login").description("sign this machine in").option("--token <t
|
|
|
23702
23626
|
${ui.note(`Listed at ${ui.link(`${where}/workstations`)}`)}
|
|
23703
23627
|
`);
|
|
23704
23628
|
});
|
|
23705
|
-
program2.command("run").description("
|
|
23706
|
-
process.exitCode = await machine.run(
|
|
23629
|
+
program2.command("run").description("pick a workstation and open a terminal on it").action(async () => {
|
|
23630
|
+
process.exitCode = await machine.run();
|
|
23707
23631
|
});
|
|
23708
23632
|
program2.command("status").description("what this machine is and what it is doing").action(async () => {
|
|
23709
23633
|
const { machine: found } = await machine.status();
|
|
@@ -23716,9 +23640,6 @@ program2.command("status").description("what this machine is and what it is doin
|
|
|
23716
23640
|
ui.say(ui.field("Session", `${session.state} ${session.title ?? session.id}`));
|
|
23717
23641
|
ui.say("\n");
|
|
23718
23642
|
});
|
|
23719
|
-
program2.command("update").description("move this machine to the latest craftspace CLI").action(async () => {
|
|
23720
|
-
await machine.update();
|
|
23721
|
-
});
|
|
23722
23643
|
program2.command("logout").description("leave the team, and undo what login wrote").action(async () => {
|
|
23723
23644
|
await machine.logout();
|
|
23724
23645
|
ui.say(ui.ok("Left the team. Nothing of ours is running here."));
|
|
@@ -23733,6 +23654,7 @@ try {
|
|
|
23733
23654
|
function machineSpecs({ platform, cores, memoryGb }) {
|
|
23734
23655
|
return [platform, cores === null ? null : `${cores} vCPU`, memoryGb == null ? null : `${memoryGb} GB`].filter((part) => part !== null).join(" \xB7 ");
|
|
23735
23656
|
}
|
|
23657
|
+
var DEFAULT_URL2 = "https://craftspace.app";
|
|
23736
23658
|
function secondsSince(at) {
|
|
23737
23659
|
return Math.round((Date.now() - new Date(at).getTime()) / 1e3);
|
|
23738
23660
|
}
|
package/dist/machine.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const machine: {
|
|
|
6
6
|
url: string;
|
|
7
7
|
install: boolean;
|
|
8
8
|
}): Promise<Machine>;
|
|
9
|
+
url(): Promise<string | null>;
|
|
9
10
|
signIn({ url, why, install }: {
|
|
10
11
|
url?: string;
|
|
11
12
|
why: string;
|
|
@@ -17,11 +18,7 @@ export declare const machine: {
|
|
|
17
18
|
}>;
|
|
18
19
|
beatOnce(): Promise<boolean>;
|
|
19
20
|
beatForever(): Promise<void>;
|
|
20
|
-
|
|
21
|
-
run({ argv, on }: {
|
|
22
|
-
argv: string[];
|
|
23
|
-
on?: string;
|
|
24
|
-
}): Promise<number>;
|
|
21
|
+
run(): Promise<number>;
|
|
25
22
|
daemon(): Promise<{
|
|
26
23
|
text: string;
|
|
27
24
|
ok: boolean;
|
package/dist/machine.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineSessionSchema, MachinesResponseSchema, } from '@craftspace/shared';
|
|
7
7
|
import { confirm, input, select } from '@inquirer/prompts';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { runner } from './run.js';
|
|
@@ -43,10 +43,13 @@ export const machine = {
|
|
|
43
43
|
};
|
|
44
44
|
return answer.machine;
|
|
45
45
|
},
|
|
46
|
+
async url() {
|
|
47
|
+
return (await readConfig())?.url ?? null;
|
|
48
|
+
},
|
|
46
49
|
async signIn({ url, why, install = false }) {
|
|
47
50
|
const where = url ?? (await readConfig())?.url ?? DEFAULT_URL;
|
|
48
51
|
if (!process.stdin.isTTY)
|
|
49
|
-
throw new Error(`${why} Run:
|
|
52
|
+
throw new Error(`${why} Run: cs login --token <token>`);
|
|
50
53
|
const page = `${where}/workstations`;
|
|
51
54
|
ui.say(`\n${ui.attention(why)}\n\n`);
|
|
52
55
|
if (await confirm({ message: `Open ${page} for a key?`, default: true })) {
|
|
@@ -124,23 +127,12 @@ export const machine = {
|
|
|
124
127
|
await new Promise((resolve) => setTimeout(resolve, nextDelayMs(failures, busy)));
|
|
125
128
|
}
|
|
126
129
|
},
|
|
127
|
-
async
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
ui.say(`${ui.clear()}${ui.waiting(`installing ${moved}`)}`);
|
|
135
|
-
const done = await update.toLatest();
|
|
136
|
-
ui.say(`${ui.clear()}${done === null
|
|
137
|
-
? ui.bad(`could not install ${moved}`, `run it yourself: ${CLI_INSTALL}@latest`)
|
|
138
|
-
: ui.ok(`craftspace ${ui.name(CLI_VERSION)} -> ${ui.name(done)}`)}`);
|
|
139
|
-
},
|
|
140
|
-
async run({ argv, on }) {
|
|
141
|
-
if (on === undefined)
|
|
142
|
-
return runHere(argv);
|
|
143
|
-
return runThere({ argv, on });
|
|
130
|
+
async run() {
|
|
131
|
+
const first = await signedInOrAsk();
|
|
132
|
+
const { auth, value: answer } = await withAuth(first, (live) => call({ ...live, method: 'GET', path: '/api/machines', schema: MachinesResponseSchema }));
|
|
133
|
+
if (answer.machines.length === 0)
|
|
134
|
+
return nowhereToGo(auth);
|
|
135
|
+
return openTerminal(await pick(answer.machines));
|
|
144
136
|
},
|
|
145
137
|
async daemon() {
|
|
146
138
|
if (process.env.CRAFTSPACE_NO_SERVICE === '1')
|
|
@@ -168,116 +160,57 @@ async function requireSignedIn() {
|
|
|
168
160
|
const config = await readConfig();
|
|
169
161
|
const token = await readToken();
|
|
170
162
|
if (!config || !token)
|
|
171
|
-
throw new Error('This machine is not signed in. Run:
|
|
163
|
+
throw new Error('This machine is not signed in. Run: cs login --token <token>');
|
|
172
164
|
return { url: config.url, token };
|
|
173
165
|
}
|
|
174
|
-
async function
|
|
175
|
-
const id = `local-${Date.now().toString(36)}`;
|
|
176
|
-
await writeSession({ id, state: 'working', title: argv.join(' ').slice(0, 200) });
|
|
177
|
-
try {
|
|
178
|
-
return await runner.attended(argv);
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
await clearSession(id);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async function runThere({ argv, on }) {
|
|
185
|
-
const first = await signedInOrAsk();
|
|
186
|
-
const { auth, value: target } = await withAuth(first, (live) => machineNamed({ ...live, name: on }));
|
|
187
|
-
const { url, token } = auth;
|
|
188
|
-
let run = await call({
|
|
189
|
-
url,
|
|
190
|
-
token,
|
|
191
|
-
method: 'POST',
|
|
192
|
-
path: `/api/machines/${target.id}/runs`,
|
|
193
|
-
body: { argv },
|
|
194
|
-
schema: MachineRunSchema,
|
|
195
|
-
});
|
|
196
|
-
ui.say(`${ui.name(target.name)} ${ui.dim(`· queued ${run.id}`)}\n\n`);
|
|
197
|
-
let printed = 0;
|
|
198
|
-
for (;;) {
|
|
199
|
-
if (run.output.length > printed) {
|
|
200
|
-
process.stdout.write(run.output.slice(printed));
|
|
201
|
-
printed = run.output.length;
|
|
202
|
-
}
|
|
203
|
-
if (run.state === 'done')
|
|
204
|
-
return 0;
|
|
205
|
-
if (run.state === 'failed')
|
|
206
|
-
return run.exitCode ?? 1;
|
|
207
|
-
if (run.state === 'needs_input') {
|
|
208
|
-
ui.say(`\n${ui.attention(`${target.name} is asking:`)} ${run.question ?? 'it needs a decision'}\n> `);
|
|
209
|
-
const reply = await readStdinLine();
|
|
210
|
-
if (reply === null)
|
|
211
|
-
return 130;
|
|
212
|
-
run = await call({
|
|
213
|
-
url,
|
|
214
|
-
token,
|
|
215
|
-
method: 'POST',
|
|
216
|
-
path: `/api/machines/runs/${run.id}/answer`,
|
|
217
|
-
body: { text: reply },
|
|
218
|
-
schema: MachineRunSchema,
|
|
219
|
-
});
|
|
220
|
-
printed = run.output.length;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
224
|
-
run = await readRun({ url, token, machineId: target.id, runId: run.id });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function readRun({ url, token, machineId, runId, }) {
|
|
228
|
-
const answer = await call({
|
|
229
|
-
url,
|
|
230
|
-
token,
|
|
231
|
-
method: 'GET',
|
|
232
|
-
path: `/api/machines/${machineId}/runs`,
|
|
233
|
-
schema: MachineRunsResponseSchema,
|
|
234
|
-
});
|
|
235
|
-
const found = answer.runs.find((candidate) => candidate.id === runId);
|
|
236
|
-
if (found === undefined)
|
|
237
|
-
throw new Error(`Run ${runId} is gone.`);
|
|
238
|
-
return found;
|
|
239
|
-
}
|
|
240
|
-
async function machineNamed({ url, token, name, }) {
|
|
241
|
-
const answer = await call({ url, token, method: 'GET', path: '/api/machines', schema: MachinesResponseSchema });
|
|
242
|
-
const found = answer.machines.find((candidate) => candidate.name === name || candidate.id === name);
|
|
243
|
-
if (found !== undefined)
|
|
244
|
-
return found;
|
|
245
|
-
if (answer.machines.length === 0)
|
|
246
|
-
throw new Error('This team has no workstations yet. Add one at /workstations.');
|
|
247
|
-
if (!process.stdin.isTTY) {
|
|
248
|
-
throw new Error(`No workstation called ${name}. This team has: ${answer.machines.map((m) => m.name).join(', ')}`);
|
|
249
|
-
}
|
|
250
|
-
ui.say(`\n${ui.attention(`No workstation called ${name}.`)}\n\n`);
|
|
166
|
+
async function pick(machines) {
|
|
251
167
|
return select({
|
|
252
|
-
message: '
|
|
253
|
-
choices:
|
|
254
|
-
name: `${candidate.name.padEnd(
|
|
168
|
+
message: 'Which workstation?',
|
|
169
|
+
choices: machines.map((candidate) => ({
|
|
170
|
+
name: `${candidate.name.padEnd(24)} ${candidate.state.padEnd(12)} ${specs(candidate)} ${beat(candidate)}`,
|
|
255
171
|
value: candidate,
|
|
172
|
+
description: candidate.sshAddress === null ? 'No address yet, so nothing to open' : undefined,
|
|
256
173
|
})),
|
|
174
|
+
pageSize: 12,
|
|
257
175
|
});
|
|
258
176
|
}
|
|
259
|
-
function
|
|
177
|
+
function specs({ platform, cores, memoryGb }) {
|
|
178
|
+
return [platform, cores === null ? null : `${cores} vCPU`, memoryGb === null ? null : `${memoryGb} GB`]
|
|
179
|
+
.filter((part) => part !== null)
|
|
180
|
+
.join(' \u00b7 ');
|
|
181
|
+
}
|
|
182
|
+
function beat({ lastBeatAt }) {
|
|
183
|
+
if (lastBeatAt === null)
|
|
184
|
+
return 'never beat';
|
|
185
|
+
const seconds = Math.max(0, Math.round((Date.now() - new Date(lastBeatAt).getTime()) / 1000));
|
|
186
|
+
if (seconds < 60)
|
|
187
|
+
return `beat ${seconds}s ago`;
|
|
188
|
+
if (seconds < 3600)
|
|
189
|
+
return `beat ${Math.round(seconds / 60)}m ago`;
|
|
190
|
+
return `beat ${Math.round(seconds / 3600)}h ago`;
|
|
191
|
+
}
|
|
192
|
+
async function openTerminal(target) {
|
|
193
|
+
if (target.sshAddress === null) {
|
|
194
|
+
throw new Error(`${target.name} has not told us an address yet. It reports one on its next beat.`);
|
|
195
|
+
}
|
|
196
|
+
const where = `${target.sshUser ?? 'root'}@${target.sshAddress}`;
|
|
197
|
+
ui.say(`\n${ui.dim('\u2192')} ${ui.name(target.name)} ${ui.dim(`\u00b7 ${where}`)}\n\n`);
|
|
260
198
|
return new Promise((resolve) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
resolve(line.trim() === '' ? null : line.trim());
|
|
268
|
-
};
|
|
269
|
-
const onData = (chunk) => {
|
|
270
|
-
buffer += chunk;
|
|
271
|
-
const at = buffer.indexOf('\n');
|
|
272
|
-
if (at !== -1)
|
|
273
|
-
done(buffer.slice(0, at));
|
|
274
|
-
};
|
|
275
|
-
const onEnd = () => done(buffer);
|
|
276
|
-
process.stdin.on('data', onData);
|
|
277
|
-
process.stdin.on('end', onEnd);
|
|
278
|
-
process.stdin.resume();
|
|
199
|
+
const session = spawn('ssh', ['-t', where], { stdio: 'inherit' });
|
|
200
|
+
session.on('error', (error) => {
|
|
201
|
+
ui.say(ui.bad(`could not start ssh`, error.message));
|
|
202
|
+
resolve(1);
|
|
203
|
+
});
|
|
204
|
+
session.on('exit', (code) => resolve(code ?? 0));
|
|
279
205
|
});
|
|
280
206
|
}
|
|
207
|
+
function nowhereToGo({ url }) {
|
|
208
|
+
ui.say(`\n${ui.attention('No workstations here yet.')}\n\n`);
|
|
209
|
+
ui.say(ui.field('Add one', ui.link(`${url}/workstations`)));
|
|
210
|
+
ui.say(ui.field('Or sign in', 'a Mac or Linux box with: cs login'));
|
|
211
|
+
ui.say('\n');
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
281
214
|
async function tools() {
|
|
282
215
|
if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
|
|
283
216
|
return probed.tools;
|
|
@@ -398,14 +331,6 @@ async function writeAuthorizedKeys(keys) {
|
|
|
398
331
|
function trimEnd(text) {
|
|
399
332
|
return text.replace(/\s+$/, '');
|
|
400
333
|
}
|
|
401
|
-
async function writeSession(session) {
|
|
402
|
-
const dir = path.join(machine.home(), 'sessions');
|
|
403
|
-
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
404
|
-
await writeFile(path.join(dir, `${session.id}.json`), JSON.stringify(session), { mode: 0o600 });
|
|
405
|
-
}
|
|
406
|
-
async function clearSession(id) {
|
|
407
|
-
await rm(path.join(machine.home(), 'sessions', `${id}.json`), { force: true });
|
|
408
|
-
}
|
|
409
334
|
async function writeAgentConfigs({ url, token, writes, }) {
|
|
410
335
|
const planned = writes.length > 0 ? writes : defaultWrites({ url, token });
|
|
411
336
|
const owned = [];
|
|
@@ -597,7 +522,6 @@ const MAX_REPORTED_SESSIONS = 50;
|
|
|
597
522
|
const MAX_BACKOFF_MS = 120_000;
|
|
598
523
|
const KEYS_BEGIN = '# craftspace begin';
|
|
599
524
|
const KEYS_END = '# craftspace end';
|
|
600
|
-
const POLL_INTERVAL_MS = 1_000;
|
|
601
525
|
const UPDATE_EVERY_MS = 6 * 60 * 60 * 1_000;
|
|
602
526
|
const BEATING = 'beating every 15s, outbound only, and keeps this set up';
|
|
603
527
|
const LINGER_LINE = `sudo loginctl enable-linger ${os.userInfo().username}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftspace/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sign a Mac or Linux machine into Craftspace and keep its agent setup in step.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"node": ">=20"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
+
"cs": "dist/index.js",
|
|
17
18
|
"craftspace": "dist/index.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|