@retasc/cli 1.7.1 → 1.7.2
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/commands/bind.js +107 -64
- package/package.json +1 -1
package/dist/commands/bind.js
CHANGED
|
@@ -63,6 +63,25 @@ export async function pick(label, items, render, askFn = ask) {
|
|
|
63
63
|
}
|
|
64
64
|
throw new Error("no valid choice — aborting");
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The message to show when `bind` aborts mid-run, or null when there's nothing
|
|
68
|
+
* to say. `bind` commits a new org server-side before the project step; if a
|
|
69
|
+
* later step throws, that org is stranded (no project, no key, no binding) and
|
|
70
|
+
* unreachable from the CLI. There's no CLI-callable org delete, so we surface
|
|
71
|
+
* the id and the resume command instead (RTSC-297, Option 2).
|
|
72
|
+
*
|
|
73
|
+
* It fires ONLY for an org THIS run created that never got bound. A merely
|
|
74
|
+
* SELECTED org (or one already bound before the failure) is not ours to
|
|
75
|
+
* reclaim, and returns null — nothing extra is printed.
|
|
76
|
+
*/
|
|
77
|
+
export function strandRecoveryHint(args) {
|
|
78
|
+
const { createdOrgThisRun, orgId, bindingWritten } = args;
|
|
79
|
+
if (!createdOrgThisRun || !orgId || bindingWritten)
|
|
80
|
+
return null;
|
|
81
|
+
return (`\n✗ Aborted before this folder was bound. A new org was created (${orgId}) ` +
|
|
82
|
+
`but has no project or key yet — re-run to finish:\n` +
|
|
83
|
+
` retasc bind --org-id ${orgId}`);
|
|
84
|
+
}
|
|
66
85
|
export async function bindAction(opts) {
|
|
67
86
|
const cfg = loadConfig();
|
|
68
87
|
const cwd = process.cwd();
|
|
@@ -106,10 +125,20 @@ export async function bindAction(opts) {
|
|
|
106
125
|
}
|
|
107
126
|
}
|
|
108
127
|
// --- resolve org (pick / flag / create) ------------------------------------
|
|
128
|
+
// Track whether THIS run created the org (vs. selected a pre-existing one or
|
|
129
|
+
// took one via --org-id). An org is created and committed server-side BEFORE
|
|
130
|
+
// the project step runs, so any abort in the project/mint/bind steps below
|
|
131
|
+
// strands it: no project, no key, no local binding — invisible from the CLI,
|
|
132
|
+
// and (orgs land on the billing rail) accumulating against the account on
|
|
133
|
+
// repeated aborts. There's no CLI-callable org delete, so instead we name the
|
|
134
|
+
// stranded org in the error and print the command to resume (RTSC-297). A
|
|
135
|
+
// merely-SELECTED org must never trigger this — it isn't ours to reclaim.
|
|
109
136
|
let orgId = opts.orgId;
|
|
137
|
+
let createdOrgThisRun = false;
|
|
110
138
|
if (!orgId && opts.orgName) {
|
|
111
139
|
const org = (await api.createOrg({ name: opts.orgName }));
|
|
112
140
|
orgId = org.orgId;
|
|
141
|
+
createdOrgThisRun = true;
|
|
113
142
|
console.log(`✓ Created org "${opts.orgName}".`);
|
|
114
143
|
}
|
|
115
144
|
if (!orgId) {
|
|
@@ -125,6 +154,7 @@ export async function bindAction(opts) {
|
|
|
125
154
|
if (!name)
|
|
126
155
|
throw new Error("org name required");
|
|
127
156
|
orgId = (await api.createOrg({ name })).orgId;
|
|
157
|
+
createdOrgThisRun = true;
|
|
128
158
|
console.log(`✓ Created org "${name}".`);
|
|
129
159
|
}
|
|
130
160
|
}
|
|
@@ -135,76 +165,89 @@ export async function bindAction(opts) {
|
|
|
135
165
|
throw new Error("no org selected — pass --org-id <id> or --org-name <name> (or run interactively).");
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
// Everything from here to the binding write is the strand window: the org
|
|
169
|
+
// exists but the workspace isn't bound yet. If we abort in it after creating
|
|
170
|
+
// the org this run, name it and print the resume command (RTSC-297).
|
|
171
|
+
let bindingWritten = false;
|
|
172
|
+
try {
|
|
173
|
+
// --- resolve project (pick / flag / create) --------------------------------
|
|
174
|
+
let projectId = opts.projectId;
|
|
175
|
+
let prefix;
|
|
176
|
+
if (!projectId && opts.project && opts.prefix) {
|
|
177
|
+
const p = (await api.createProject({ orgId: orgId, name: opts.project, prefix: opts.prefix }));
|
|
178
|
+
projectId = p.projectId;
|
|
179
|
+
prefix = p.prefix;
|
|
180
|
+
console.log(`✓ Created project ${p.prefix}.`);
|
|
181
|
+
}
|
|
182
|
+
if (!projectId) {
|
|
183
|
+
const { projects } = (await api.listProjects({ orgId: orgId }));
|
|
184
|
+
const list = projects ?? [];
|
|
185
|
+
if (isInteractive()) {
|
|
186
|
+
const chosen = await pick("Select a project", list, (p) => `${p.prefix} — ${p.name}`);
|
|
187
|
+
if (chosen) {
|
|
188
|
+
projectId = chosen.id;
|
|
189
|
+
prefix = chosen.prefix;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const name = await ask("New project name: ");
|
|
193
|
+
const pfx = (await ask("Project prefix (e.g. ACME): ")).toUpperCase();
|
|
194
|
+
if (!name || !pfx)
|
|
195
|
+
throw new Error("project name and prefix required");
|
|
196
|
+
const p = (await api.createProject({ orgId: orgId, name, prefix: pfx }));
|
|
197
|
+
projectId = p.projectId;
|
|
198
|
+
prefix = p.prefix;
|
|
199
|
+
console.log(`✓ Created project ${p.prefix}.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (list.length === 1) {
|
|
203
|
+
projectId = list[0].id;
|
|
204
|
+
prefix = list[0].prefix;
|
|
155
205
|
}
|
|
156
206
|
else {
|
|
157
|
-
|
|
158
|
-
const pfx = (await ask("Project prefix (e.g. ACME): ")).toUpperCase();
|
|
159
|
-
if (!name || !pfx)
|
|
160
|
-
throw new Error("project name and prefix required");
|
|
161
|
-
const p = (await api.createProject({ orgId: orgId, name, prefix: pfx }));
|
|
162
|
-
projectId = p.projectId;
|
|
163
|
-
prefix = p.prefix;
|
|
164
|
-
console.log(`✓ Created project ${p.prefix}.`);
|
|
207
|
+
throw new Error("no project selected — pass --project-id <id>, or --project <name> --prefix <PFX>.");
|
|
165
208
|
}
|
|
166
209
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
// --- mint a key for THIS (org, project) and wire the watchdog into THIS folder
|
|
211
|
+
const minted = (await api.mintKey({
|
|
212
|
+
orgId: orgId,
|
|
213
|
+
projectId: projectId,
|
|
214
|
+
agentName: opts.agent,
|
|
215
|
+
runtime: opts.runtime ?? "claude-code",
|
|
216
|
+
keyName: prefix ? `${prefix} key` : undefined,
|
|
217
|
+
}));
|
|
218
|
+
console.log(`✓ Minted key for this workspace (${minted.key.slice(0, 14)}…).\n`);
|
|
219
|
+
// RTSC-92: the secret stays OUT of the repo. Store it in the home keystore
|
|
220
|
+
// keyed by a workspace id; reuse this folder's existing id (so a re-bind, or a
|
|
221
|
+
// teammate's committed marker, keeps the same id) or mint a fresh one.
|
|
222
|
+
const workspaceId = existing?.workspaceId ?? newWorkspaceId();
|
|
223
|
+
setBinding(workspaceId, {
|
|
224
|
+
orgId: orgId,
|
|
225
|
+
projectId: projectId,
|
|
226
|
+
key: minted.key,
|
|
227
|
+
url: cfg.mcpUrl,
|
|
228
|
+
prefix,
|
|
229
|
+
orgName: undefined,
|
|
230
|
+
boundPath: cwd,
|
|
231
|
+
createdAt: Date.now(),
|
|
232
|
+
});
|
|
233
|
+
bindingWritten = true; // strand window closed — the org is now reachable
|
|
234
|
+
// Per-folder only (local scope), always watchdog. The marker carries only the
|
|
235
|
+
// workspace id — no secret — so ./.mcp.json is safe to commit.
|
|
236
|
+
installMarker({ workspaceId, scope: "local" });
|
|
237
|
+
// Confirm the binding the same way the agent will see it.
|
|
238
|
+
try {
|
|
239
|
+
const b = await resolveBinding(cfg.mcpUrl, minted.key);
|
|
240
|
+
console.log(`\n✓ This folder is bound to org "${b.org.name}" / project ${b.project.prefix}.\n` +
|
|
241
|
+
` Agents launched here can only ever read or write ${b.project.prefix}.`);
|
|
170
242
|
}
|
|
171
|
-
|
|
172
|
-
|
|
243
|
+
catch {
|
|
244
|
+
/* binding written; whoami confirmation is best-effort */
|
|
173
245
|
}
|
|
174
246
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
runtime: opts.runtime ?? "claude-code",
|
|
181
|
-
keyName: prefix ? `${prefix} key` : undefined,
|
|
182
|
-
}));
|
|
183
|
-
console.log(`✓ Minted key for this workspace (${minted.key.slice(0, 14)}…).\n`);
|
|
184
|
-
// RTSC-92: the secret stays OUT of the repo. Store it in the home keystore
|
|
185
|
-
// keyed by a workspace id; reuse this folder's existing id (so a re-bind, or a
|
|
186
|
-
// teammate's committed marker, keeps the same id) or mint a fresh one.
|
|
187
|
-
const workspaceId = existing?.workspaceId ?? newWorkspaceId();
|
|
188
|
-
setBinding(workspaceId, {
|
|
189
|
-
orgId: orgId,
|
|
190
|
-
projectId: projectId,
|
|
191
|
-
key: minted.key,
|
|
192
|
-
url: cfg.mcpUrl,
|
|
193
|
-
prefix,
|
|
194
|
-
orgName: undefined,
|
|
195
|
-
boundPath: cwd,
|
|
196
|
-
createdAt: Date.now(),
|
|
197
|
-
});
|
|
198
|
-
// Per-folder only (local scope), always watchdog. The marker carries only the
|
|
199
|
-
// workspace id — no secret — so ./.mcp.json is safe to commit.
|
|
200
|
-
installMarker({ workspaceId, scope: "local" });
|
|
201
|
-
// Confirm the binding the same way the agent will see it.
|
|
202
|
-
try {
|
|
203
|
-
const b = await resolveBinding(cfg.mcpUrl, minted.key);
|
|
204
|
-
console.log(`\n✓ This folder is bound to org "${b.org.name}" / project ${b.project.prefix}.\n` +
|
|
205
|
-
` Agents launched here can only ever read or write ${b.project.prefix}.`);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
/* binding written; whoami confirmation is best-effort */
|
|
247
|
+
catch (err) {
|
|
248
|
+
const hint = strandRecoveryHint({ createdOrgThisRun, orgId, bindingWritten });
|
|
249
|
+
if (hint)
|
|
250
|
+
console.error(hint);
|
|
251
|
+
throw err;
|
|
209
252
|
}
|
|
210
253
|
}
|
package/package.json
CHANGED