cofluxd 2.0.2 → 2.1.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/account-client.mjs +69 -2
- package/coflux.mjs +28 -2
- package/package.json +1 -1
- package/skills/coflux/SKILL.md +108 -13
package/account-client.mjs
CHANGED
|
@@ -3,6 +3,50 @@ import fs from "node:fs";
|
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
/* --------------------------------- 实体标识 -------------------------------- */
|
|
7
|
+
// `coflux:<kind>:<hex>`:设备 / 项目 / 工作区 / 终端 ID 的可粘贴短形式,hex 是 ID 的前几位
|
|
8
|
+
// (生成时固定取前 8 位)。解析大小写不敏感并归一成小写;凡是收 ID 的地方都收标识。
|
|
9
|
+
//
|
|
10
|
+
// 规则是纯拼接,故各端各自本地生成,不上协议:Rust 侧同一份规则在 crates/cli/src/handle.rs 与
|
|
11
|
+
// crates/worker/src/handle.rs——两版 CLI 的输出是逐字对齐的契约,改一边必须改另一边。
|
|
12
|
+
const HANDLE_KINDS = ["device", "project", "workspace", "terminal"];
|
|
13
|
+
|
|
14
|
+
/** `id` 的标识。空进空出:缺坐标时不能造出 `coflux:x:` 这样的半截标识。 */
|
|
15
|
+
export function entityHandle(kind, id) {
|
|
16
|
+
const raw = typeof id === "string" ? id : "";
|
|
17
|
+
return raw ? `coflux:${kind}:${raw.slice(0, 8).toLowerCase()}` : "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 解析标识;不是标识(裸 UUID 也一样)返回 null,调用方按它看起来的那个 ID 处理。 */
|
|
21
|
+
export function parseHandle(raw) {
|
|
22
|
+
if (typeof raw !== "string") return null;
|
|
23
|
+
const parts = raw.toLowerCase().split(":");
|
|
24
|
+
if (parts.length !== 3 || parts[0] !== "coflux") return null;
|
|
25
|
+
const [, kind, hex] = parts;
|
|
26
|
+
if (!HANDLE_KINDS.includes(kind) || !/^[0-9a-f]{4,32}$/.test(hex)) return null;
|
|
27
|
+
return { kind, prefix: hex };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `--device` / `--workspace` 筛选值命中某个 ID 吗?标识按类型 + 前缀比,其余按原样相等比。
|
|
32
|
+
* 这是**比较**不是解析:不查表,只用同一套语法——也正因为如此,标识绝不能漏到字符串相等那条
|
|
33
|
+
* 路上去,否则它谁也匹配不上,打印一个空列表还不报错。
|
|
34
|
+
*/
|
|
35
|
+
export function matchesTarget(target, id, kind) {
|
|
36
|
+
const handle = parseHandle(target);
|
|
37
|
+
if (!handle) return id === target;
|
|
38
|
+
return handle.kind === kind && typeof id === "string" && id.toLowerCase().startsWith(handle.prefix);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 筛选参数拿到了别的类型的标识:说清楚,不要打印空列表。不是标识的一律放行(那就是个 ID)。 */
|
|
42
|
+
const HANDLE_LABELS = { device: "设备", project: "项目", workspace: "工作区", terminal: "终端" };
|
|
43
|
+
export function checkFilterHandle(flag, expected, target) {
|
|
44
|
+
const handle = parseHandle(target);
|
|
45
|
+
if (handle && handle.kind !== expected) {
|
|
46
|
+
throw new Error(`--${flag} 需要${HANDLE_LABELS[expected]}标识或${HANDLE_LABELS[expected]} ID,给的是${HANDLE_LABELS[handle.kind]}标识 ${target}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
6
50
|
function origin(raw) {
|
|
7
51
|
const url = new URL(raw.replace(/^wss:/, "https:").replace(/^ws:/, "http:"));
|
|
8
52
|
if (url.username || url.password) throw new Error("服务器地址不能包含凭据");
|
|
@@ -45,6 +89,22 @@ export async function runAccountCommand(positionals, flags, home) {
|
|
|
45
89
|
const sessionPath = join(home, "cli-session.json");
|
|
46
90
|
const required = (key) => { if (!flags[key]) throw new Error(`缺少 --${key}`); return flags[key]; };
|
|
47
91
|
const target = () => { if (!id) throw new Error("缺少目标 ID"); return id; };
|
|
92
|
+
// `project import <path>`: the path is resolved on the **target device** (`~` expansion and
|
|
93
|
+
// `git rev-parse --show-toplevel` both happen there), so the CLI only checks its shape — the same
|
|
94
|
+
// rule as `device exec --cwd`. Expanding it here would resolve the caller's home on the wrong machine.
|
|
95
|
+
const importPath = () => {
|
|
96
|
+
const value = (id ?? "").trim();
|
|
97
|
+
if (!value) throw new Error('缺少要导入的路径(导入当前目录写 coflux project import "$PWD")');
|
|
98
|
+
if (!(value.startsWith("/") || value === "~" || value.startsWith("~/"))) throw new Error('路径要绝对路径或 ~ 开头(它在目标设备上解析);导入当前目录写 coflux project import "$PWD"');
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
// `--device` falls back to the daemon-issued COFLUX_DEVICE_ID; empty on both sides is an error,
|
|
102
|
+
// never a guess — silently importing onto the wrong machine is worse than failing.
|
|
103
|
+
const deviceTarget = () => {
|
|
104
|
+
const value = (flags.device || process.env.COFLUX_DEVICE_ID || "").trim();
|
|
105
|
+
if (!value) throw new Error("缺少设备:请加 --device <id>(coflux device list 可以看到)");
|
|
106
|
+
return value;
|
|
107
|
+
};
|
|
48
108
|
const print = (value) => console.log(JSON.stringify(value));
|
|
49
109
|
if (command === "login") {
|
|
50
110
|
const server = origin(flags.server || "https://api.coflux.dev");
|
|
@@ -109,6 +169,9 @@ export async function runAccountCommand(positionals, flags, home) {
|
|
|
109
169
|
process.exit(exitCode);
|
|
110
170
|
}
|
|
111
171
|
let operation;
|
|
172
|
+
if (command === "project") {
|
|
173
|
+
if (sub === "import") operation = { op: "project.import", daemonId: deviceTarget(), path: importPath(), ...(flags.name ? { name: flags.name } : {}) };
|
|
174
|
+
}
|
|
112
175
|
if (command === "workspace") {
|
|
113
176
|
if (sub === "new") operation = { op: "workspace.new", projectId: required("project"), branch: required("branch"), createNew: !flags["existing-branch"], ...(flags.name ? { name: flags.name } : {}) };
|
|
114
177
|
if (sub === "rename") operation = { op: "workspace.rename", workspaceId: target(), name: required("name") };
|
|
@@ -124,12 +187,16 @@ export async function runAccountCommand(positionals, flags, home) {
|
|
|
124
187
|
if (["stop", "remove"].includes(sub)) operation = { op: `terminal.${sub}`, terminalId: target() };
|
|
125
188
|
}
|
|
126
189
|
if (!operation && command !== "whoami" && command !== "ports" && sub !== "list") throw new Error("未知账号命令");
|
|
127
|
-
|
|
190
|
+
// `project.import` is addressed by device like `snapshot`, so `--device` is an input to it rather than a filter.
|
|
191
|
+
if (operation && ((flags.device && operation.op !== "project.import") || (flags.workspace && operation.op !== "terminal.new"))) throw new Error("目标 ID 已确定作用范围,请不要附加设备或工作区筛选参数");
|
|
128
192
|
let value = await call(operation || { op: "snapshot" });
|
|
129
193
|
if (command === "whoami") value = { accountId: value.accountId };
|
|
130
194
|
else if (sub === "list" || command === "ports") {
|
|
131
195
|
const field = { device: "devices", project: "projects", workspace: "workspaces", terminal: "terminals", ports: "ports" }[command];
|
|
132
|
-
|
|
196
|
+
// 客户端字符串比较,不经中心解析:标识不在这里认,就会一个都匹配不上(空列表、还不报错)。
|
|
197
|
+
if (flags.device) checkFilterHandle("device", "device", flags.device);
|
|
198
|
+
if (flags.workspace) checkFilterHandle("workspace", "workspace", flags.workspace);
|
|
199
|
+
value = value[field].filter((item) => (!flags.device || matchesTarget(flags.device, item.daemonId, "device")) && (!flags.workspace || matchesTarget(flags.workspace, item.workspaceId, "workspace") || (command === "workspace" && matchesTarget(flags.workspace, item.id, "workspace"))));
|
|
133
200
|
}
|
|
134
201
|
print(value);
|
|
135
202
|
}
|
package/coflux.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// coflux:账号与本地、跨设备业务操作;不负责宿主生命周期。
|
|
3
|
-
import { handlesAccountCommand, runAccountCommand } from "./account-client.mjs";
|
|
3
|
+
import { entityHandle, handlesAccountCommand, runAccountCommand } from "./account-client.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -206,6 +206,15 @@ function tailLines(text, n) {
|
|
|
206
206
|
return lines.slice(-n).join("\n");
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* 一行终端的第一列:标识。daemon 已经在载荷里给了 `ref`;它没给(CLI 比 daemon 新)就按同一条
|
|
211
|
+
* 规则从 `taskId` 现算一个——生成规则是纯拼接,两边算出来的东西一样。
|
|
212
|
+
* 与 Rust 版 `row_handle`(crates/cli/src/commands.rs)逐字对齐。
|
|
213
|
+
*/
|
|
214
|
+
function rowHandle(t) {
|
|
215
|
+
return (typeof t.ref === "string" && t.ref) || entityHandle("terminal", t.taskId);
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
/** ` busy` / ` idle` plus ` last=<code>` for a live, instrumented terminal; nothing otherwise. */
|
|
210
219
|
function commandSuffix(t) {
|
|
211
220
|
if (!t.integrated) return "";
|
|
@@ -256,7 +265,7 @@ async function cmdTerminal(values) {
|
|
|
256
265
|
if (!terminals.length) return void console.log("本工作区暂无终端");
|
|
257
266
|
for (const t of terminals) {
|
|
258
267
|
const exit = t.exitCode === undefined || t.exitCode === null ? "" : ` exit=${t.exitCode}`;
|
|
259
|
-
console.log(`${t
|
|
268
|
+
console.log(`${rowHandle(t)} ${t.status}${exit}${commandSuffix(t)} ${t.title}`);
|
|
260
269
|
}
|
|
261
270
|
} else if (sub === "read") {
|
|
262
271
|
const taskId = positionals[2];
|
|
@@ -330,10 +339,14 @@ async function cmdWorkspace() {
|
|
|
330
339
|
const sub = positionals[1];
|
|
331
340
|
if (!sub) {
|
|
332
341
|
const result = await agentPost({ action: "workspace.current" });
|
|
342
|
+
// ref / owningRef 原样透传:daemon 旧到不给就是 undefined,JSON.stringify 直接省掉这两个键
|
|
343
|
+
// (与 Rust 版 render_workspace_current 同序同省略规则)。
|
|
333
344
|
return void console.log(JSON.stringify({
|
|
334
345
|
workspaceId: result.workspaceId,
|
|
346
|
+
ref: result.ref,
|
|
335
347
|
path: result.path,
|
|
336
348
|
owningWorkspaceId: result.owningWorkspaceId,
|
|
349
|
+
owningRef: result.owningRef,
|
|
337
350
|
moved: Boolean(result.moved),
|
|
338
351
|
}));
|
|
339
352
|
}
|
|
@@ -522,6 +535,11 @@ const HELP = `coflux —— 账号与终端操作
|
|
|
522
535
|
该 worktree 已被删掉:其下所有终端搬回项目主工作区、工作区记录消失
|
|
523
536
|
(不执行 git worktree remove)
|
|
524
537
|
|
|
538
|
+
实体标识:设备 / 项目 / 工作区 / 终端的 ID 都可以写成 coflux:<kind>:<ID 前 8 位>,例如
|
|
539
|
+
coflux:workspace:3f2a1b7c。凡是收 ID 的地方都收标识(大小写不敏感),返回实体的地方都带一个
|
|
540
|
+
ref 字段给出它的标识。前缀在范围内撞车时会让你改用完整 ID;标识类型与命令要的不一致会直接报错,
|
|
541
|
+
不会去动旁边那个实体。
|
|
542
|
+
|
|
525
543
|
agent 命令的环境变量:COFLUX_AGENT_TIMEOUT_MS 收窄单次请求的等待上限(默认 30000,只能调小),
|
|
526
544
|
供有硬超时的 hook 脚本用——到点干净失败,好过被宿主杀在半路。
|
|
527
545
|
|
|
@@ -537,6 +555,14 @@ agent 命令的环境变量:COFLUX_AGENT_TIMEOUT_MS 收窄单次请求的等
|
|
|
537
555
|
任何工作区。--cwd 默认 daemon 用户的 HOME,只接受绝对路径或 ~ 开头的路径;
|
|
538
556
|
--timeout 默认 60 秒、最长 600 秒;没有 stdin。要输密码、驱动 TUI,或想让
|
|
539
557
|
用户看见过程并能接管的长任务,用 coflux terminal new,不要用它
|
|
558
|
+
coflux project import <path> [--device <id>] [--name <名称>]
|
|
559
|
+
把设备上的一个 git 仓库目录变成项目(路径在仓库里就导入仓库根),并
|
|
560
|
+
建好它的主工作区;打印一行 JSON:projectId / name / repoPath /
|
|
561
|
+
defaultBranch / workspaceId / path / alreadyImported。<path> 必填,
|
|
562
|
+
只接受绝对路径或 ~ 开头的路径(它在目标设备上解析)——导入当前目录写
|
|
563
|
+
coflux project import "$PWD"。--device 缺省取 COFLUX_DEVICE_ID。
|
|
564
|
+
同一个仓库根导入第二次不会多出一个项目:返回已有的那个,
|
|
565
|
+
alreadyImported=true
|
|
540
566
|
coflux workspace new --project <id> --branch <分支> [--existing-branch]
|
|
541
567
|
coflux workspace rename <id> --name <名称> | workspace remove <id>
|
|
542
568
|
coflux terminal new --workspace <id> [--cmd <命令>]
|
package/package.json
CHANGED
package/skills/coflux/SKILL.md
CHANGED
|
@@ -59,6 +59,11 @@ env | grep '^COFLUX_'
|
|
|
59
59
|
| `COFLUX_TASK_ID` | id of this terminal (the taskId / terminalId used by local commands and `read_terminal`) |
|
|
60
60
|
| `COFLUX_SESSION_ID` | id of this PTY session |
|
|
61
61
|
|
|
62
|
+
Under those id lines the `<coflux-session>` block prints a `<Kind> handle:` line for the device,
|
|
63
|
+
project, workspace and terminal — the same ids in a short, typed, pasteable form (see "Entity
|
|
64
|
+
handles"). A PTY session has no handle, a coordinate that is empty contributes no line, and the
|
|
65
|
+
`COFLUX_*` variables themselves are always full UUIDs.
|
|
66
|
+
|
|
62
67
|
- **Variables empty or absent**: there is no local terminal context. Use `coflux whoami` to check
|
|
63
68
|
account access and the account CLI to discover workspaces. Ask the user to log in when needed;
|
|
64
69
|
never fabricate COFLUX_* coordinates.
|
|
@@ -138,9 +143,9 @@ So, after entering or leaving a worktree, owning **and** effective are both the
|
|
|
138
143
|
its id to account CLI commands and everything local already acts on it. The plugin drops the new id next to the
|
|
139
144
|
tool result, and `coflux workspace` always tells you. Two things stay behind on purpose:
|
|
140
145
|
|
|
141
|
-
- `COFLUX_WORKSPACE_ID` (and the id in the `<coflux-session>` block from
|
|
142
|
-
still names where the terminal was *opened*; it is frozen when the PTY
|
|
143
|
-
rewritten. Never reuse
|
|
146
|
+
- `COFLUX_WORKSPACE_ID` (and the workspace id and handle in the `<coflux-session>` block from
|
|
147
|
+
earlier in this session) still names where the terminal was *opened*; it is frozen when the PTY
|
|
148
|
+
starts and cannot be rewritten. Never reuse either form after a move.
|
|
144
149
|
- The shell inside this terminal keeps its own directory. That is only about the shell; it does not
|
|
145
150
|
affect where your work is attributed.
|
|
146
151
|
|
|
@@ -152,15 +157,75 @@ a daemon that is down or too old — the session just carries on with the owners
|
|
|
152
157
|
|
|
153
158
|
```sh
|
|
154
159
|
coflux workspace
|
|
155
|
-
{"workspaceId":"
|
|
160
|
+
{"workspaceId":"3f2a1b7c-9d44-4e1a-8f02-1b6c2d5e7a90","ref":"coflux:workspace:3f2a1b7c","path":"/Users/me/.coflux/worktrees/ws-b","owningWorkspaceId":"7c1d90ab-5e33-4c88-9a71-0d4f8b2e6c15","owningRef":"coflux:workspace:7c1d90ab","moved":true}
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
One line of JSON: `workspaceId` (+ `path`) is the **effective** workspace, `owningWorkspaceId` is the
|
|
159
|
-
workspace this terminal belongs to right now, and `
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`
|
|
164
|
+
workspace this terminal belongs to right now, `ref` and `owningRef` are those two workspaces'
|
|
165
|
+
handles (a daemon too old to send them leaves both keys out), and `moved` says whether they differ.
|
|
166
|
+
With the plugin installed you also get a `<coflux-session-moved>` block at the start of every prompt
|
|
167
|
+
while the two differ — but that block only arrives with the **next** user prompt. **About to call an
|
|
168
|
+
account command right after a `cd`? Run `coflux workspace` first** and use the `workspaceId` it
|
|
169
|
+
prints; do not reuse `COFLUX_WORKSPACE_ID`.
|
|
170
|
+
|
|
171
|
+
## Entity handles: ids you can paste
|
|
172
|
+
|
|
173
|
+
Every coflux entity also has a **handle**: one short string that says both "this is coflux" and
|
|
174
|
+
"this is a device / project / workspace / terminal".
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
coflux:device:b6767697
|
|
178
|
+
coflux:project:7a83f21e
|
|
179
|
+
coflux:workspace:3f2a1b7c
|
|
180
|
+
coflux:terminal:9e21c4d0
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The grammar is `coflux:<kind>:<short>`. `<kind>` is one of `device`, `project`, `workspace` and
|
|
184
|
+
`terminal`. `<short>` is the first 8 hexadecimal characters of the entity's UUID — its first
|
|
185
|
+
dash-delimited group. Handles are always generated in lowercase and are read case-insensitively.
|
|
186
|
+
There is nothing else encoded in one: no name, no timestamp, no device. UUIDs stay canonical; a
|
|
187
|
+
handle is another way to *write* an id, not a replacement for it.
|
|
188
|
+
|
|
189
|
+
**Anywhere an id is accepted, a handle is accepted.** That covers every account CLI command taking
|
|
190
|
+
a device, project, workspace or terminal id, the `--device` / `--workspace` filters of the list
|
|
191
|
+
commands, and every local terminal command taking a terminal id. `coflux terminal read
|
|
192
|
+
coflux:terminal:9e21c4d0` and `coflux terminal read <that terminal's full UUID>` are the same call.
|
|
193
|
+
|
|
194
|
+
**Anywhere an entity is returned, its handle comes back too**, in a `ref` field beside the
|
|
195
|
+
entity's unchanged id field. The ids in JSON — `taskId`, `workspaceId`, `deviceId` — are still full
|
|
196
|
+
UUIDs, and `ref` is the handle next to them; where one payload carries two ids of the same kind it
|
|
197
|
+
gets two keys, as `coflux workspace` does with `ref` and `owningRef`. Local terminal results carry
|
|
198
|
+
both as well, and their `taskId` is always the **resolved** UUID — never the handle you passed in,
|
|
199
|
+
so it is safe to feed onward. The local `coflux terminal list` text rows lead with the handle, so
|
|
200
|
+
the next command can be typed straight from what you just read.
|
|
201
|
+
|
|
202
|
+
So when the user pastes you a `coflux:` string, you already know what kind of thing it names and
|
|
203
|
+
can use it as-is — no listing, no guessing. Handles are made to be pasted rather than displayed:
|
|
204
|
+
the desktop and iOS clients offer "copy handle" on the entity (right-click in the desktop sidebar
|
|
205
|
+
and terminal tabs, long-press on iOS rows and terminal chips) and show no handle text otherwise.
|
|
206
|
+
|
|
207
|
+
### When a handle does not resolve
|
|
208
|
+
|
|
209
|
+
A handle is a prefix, so resolving one can fail in three distinct ways. Each is one readable
|
|
210
|
+
sentence; what matters is which of the three you got:
|
|
211
|
+
|
|
212
|
+
- **Nothing matches** — no entity of that kind, within the scope that command can see, starts with
|
|
213
|
+
that short id. The handle belongs to another account, to a workspace you are not in, or to
|
|
214
|
+
something that no longer exists. List and pick again; re-running the same string cannot help.
|
|
215
|
+
- **More than one matches** — two entities of that kind share the short id, so nothing is acted on.
|
|
216
|
+
Use the full UUID instead. There is no "first match" fallback, and picking one yourself is
|
|
217
|
+
exactly what the error exists to prevent.
|
|
218
|
+
- **Wrong kind** — the handle is well-formed but names a different kind than the command expects
|
|
219
|
+
(a workspace handle where a terminal id goes). The command fails rather than act on an adjacent
|
|
220
|
+
entity; the fix is a handle of the expected kind, never a retry.
|
|
221
|
+
|
|
222
|
+
The scopes differ on purpose: account CLI commands resolve a handle among **the requesting
|
|
223
|
+
account's** entities, while local terminal commands resolve it among the terminals of **the
|
|
224
|
+
workspace your cwd is in** — the same boundary that already makes another workspace's terminal
|
|
225
|
+
indistinguishable from one that does not exist.
|
|
226
|
+
|
|
227
|
+
Handles need a daemon and a CLI new enough to know about them. An older one rejects a handle the
|
|
228
|
+
way it rejects any unrecognised id; there, use the full UUID and tell the user to upgrade.
|
|
164
229
|
|
|
165
230
|
## When to open a terminal
|
|
166
231
|
|
|
@@ -266,9 +331,10 @@ coflux terminal read <taskId> # the last 200 lines of the scrollback
|
|
|
266
331
|
coflux terminal read <taskId> --lines=50
|
|
267
332
|
```
|
|
268
333
|
|
|
269
|
-
`list` rows are `<
|
|
270
|
-
|
|
271
|
-
|
|
334
|
+
`list` rows are `<handle> <state>[ exit=<code>][ busy|idle][ last=<code>] <title>`: the first
|
|
335
|
+
column is the terminal's handle (see "Entity handles"), which every other terminal command accepts
|
|
336
|
+
as its id; `running` / `exited` / `idle` is the terminal, `busy` or `idle` says whether a command is
|
|
337
|
+
running in it right now, and `last=<code>` is the exit code of the last command that finished. `read` returns the tail
|
|
272
338
|
of the terminal's **full scrollback** (well beyond one screen, up to the daemon's history limit),
|
|
273
339
|
ANSI stripped, with a `# running` / `# exited exit=<code>` header. A freshly opened terminal can
|
|
274
340
|
read back empty for a moment while the shell starts. Once the shell has exited only the last
|
|
@@ -400,7 +466,10 @@ there: the terminal is open all the same, use `read` and `send` with it; "busy"
|
|
|
400
466
|
still running in that terminal, `wait` for it or `read` first; "unknown action terminal.run" = this
|
|
401
467
|
machine's daemon is older than the CLI, tell the user to run `cofluxd update && cofluxd restart`
|
|
402
468
|
(the terminal was opened as a plain shell, nothing was run); "daemon is not connected to the
|
|
403
|
-
center" only appears on `new`/`list`/`ports`/`notify`, retry once it reconnects.
|
|
469
|
+
center" only appears on `new`/`list`/`ports`/`notify`, retry once it reconnects. A terminal handle
|
|
470
|
+
that names the wrong kind, or whose prefix matches more than one terminal in this workspace, has
|
|
471
|
+
its own sentence each — see "When a handle does not resolve"; an unknown handle shares the
|
|
472
|
+
"not in this workspace or does not exist" sentence above, on purpose.
|
|
404
473
|
|
|
405
474
|
## Account CLI: across workspaces and devices
|
|
406
475
|
|
|
@@ -412,6 +481,7 @@ never as a command argument. Account commands return JSON. A workspace ID identi
|
|
|
412
481
|
coflux device list
|
|
413
482
|
coflux device exec <deviceId> --cmd="<command>" [--cwd=<dir>] [--timeout=<seconds>]
|
|
414
483
|
coflux project list --device <deviceId>
|
|
484
|
+
coflux project import <path> [--device <deviceId>] [--name <name>]
|
|
415
485
|
coflux workspace list --device <deviceId>
|
|
416
486
|
coflux workspace new --project <projectId> --branch <branch>
|
|
417
487
|
coflux terminal new --workspace <workspaceId> --title <title> [--cmd <command>]
|
|
@@ -437,6 +507,31 @@ Read before sending; stop immediately when the user takes over. If a write times
|
|
|
437
507
|
result before retrying. Exiting the CLI does not stop its terminals. Delete workspaces through
|
|
438
508
|
`coflux workspace remove` so the filesystem and workspace records stay consistent.
|
|
439
509
|
|
|
510
|
+
### Turn a repository into a project
|
|
511
|
+
|
|
512
|
+
```sh
|
|
513
|
+
coflux project import "$PWD"
|
|
514
|
+
coflux project import /srv/checkouts/api --device <deviceId> --name api
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
A repository that is not a project yet has no workspaces, so nothing else in this skill can reach
|
|
518
|
+
it: `workspace new` needs a `projectId`. This is the command that creates one, without the user
|
|
519
|
+
having to click Import in the desktop app.
|
|
520
|
+
|
|
521
|
+
`<path>` is required and must be **absolute or start with `~/`** — it is resolved on the target
|
|
522
|
+
device, so importing the current directory is written explicitly as `"$PWD"`. Any path inside the
|
|
523
|
+
repository imports the repository root. `--device` selects the machine and defaults to
|
|
524
|
+
`COFLUX_DEVICE_ID`; with neither, the command fails rather than guessing. `--name` overrides the
|
|
525
|
+
project name, which otherwise comes from the git remote and falls back to the directory name.
|
|
526
|
+
|
|
527
|
+
Success is one line of JSON: `projectId`, `name`, `repoPath`, `defaultBranch`, the main workspace's
|
|
528
|
+
`workspaceId` and `path`, and `alreadyImported`. A repository root on a device has exactly one
|
|
529
|
+
project, so importing the same repository twice returns the existing one with
|
|
530
|
+
`alreadyImported: true` and creates nothing — re-running it is safe. Failures are one sentence and a
|
|
531
|
+
non-zero exit: "不是 git 仓库" (the path is not inside a repository), "设备离线,无法执行该操作"
|
|
532
|
+
(that device is not connected), and a submitted-but-unfinished import tells you to check
|
|
533
|
+
`coflux project list`.
|
|
534
|
+
|
|
440
535
|
### Run one command on another machine
|
|
441
536
|
|
|
442
537
|
```sh
|