@vincentt-xr/cli 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/cli/args.d.ts +14 -0
- package/dist/cli/args.js +51 -0
- package/dist/cli/index.js +416 -100
- package/dist/lib/backend.d.ts +44 -20
- package/dist/lib/backend.js +98 -51
- package/dist/lib/diagnostics.d.ts +27 -2
- package/dist/lib/diagnostics.js +73 -8
- package/dist/lib/initCommand.d.ts +14 -0
- package/dist/lib/initCommand.js +75 -0
- package/dist/lib/lifecycle.d.ts +85 -19
- package/dist/lib/lifecycle.js +133 -130
- package/dist/lib/org.d.ts +29 -0
- package/dist/lib/org.js +45 -0
- package/dist/lib/projects.d.ts +82 -0
- package/dist/lib/projects.js +217 -0
- package/dist/lib/relayInfo.d.ts +41 -3
- package/dist/lib/relayInfo.js +94 -0
- package/dist/lib/whoami.d.ts +11 -0
- package/dist/lib/whoami.js +38 -0
- package/dist/login/authBackend.d.ts +30 -0
- package/dist/login/authBackend.js +80 -0
- package/dist/login/guard.d.ts +13 -0
- package/dist/login/guard.js +17 -0
- package/dist/login/loginCommand.d.ts +39 -0
- package/dist/login/loginCommand.js +72 -0
- package/dist/login/pkce.d.ts +47 -0
- package/dist/login/pkce.js +106 -0
- package/dist/login/pkceLogin.d.ts +53 -0
- package/dist/login/pkceLogin.js +152 -0
- package/dist/mcp/server.js +52 -25
- package/dist/preview/api.d.ts +84 -0
- package/dist/preview/api.js +174 -0
- package/dist/preview/command.d.ts +96 -0
- package/dist/preview/command.js +276 -0
- package/dist/preview/copy.d.ts +209 -0
- package/dist/preview/copy.js +444 -0
- package/dist/preview/index.d.ts +2 -3
- package/dist/preview/index.js +5 -6
- package/dist/preview/proxy.d.ts +12 -1
- package/dist/preview/proxy.js +17 -7
- package/dist/preview/qr.d.ts +30 -0
- package/dist/preview/qr.js +89 -0
- package/dist/preview/runner.d.ts +155 -30
- package/dist/preview/runner.js +219 -68
- package/dist/preview/stream.d.ts +52 -0
- package/dist/preview/stream.js +171 -0
- package/dist/preview/viewers.d.ts +113 -0
- package/dist/preview/viewers.js +242 -0
- package/dist/publish/api.d.ts +67 -0
- package/dist/publish/api.js +96 -0
- package/dist/publish/bounds.d.ts +25 -0
- package/dist/publish/bounds.js +53 -0
- package/dist/publish/classify.d.ts +51 -0
- package/dist/publish/classify.js +138 -0
- package/dist/publish/command.d.ts +27 -0
- package/dist/publish/command.js +328 -0
- package/dist/publish/consoleVerbs.d.ts +17 -0
- package/dist/publish/consoleVerbs.js +26 -0
- package/dist/publish/copy.d.ts +180 -0
- package/dist/publish/copy.js +606 -0
- package/dist/publish/defaults.d.ts +16 -0
- package/dist/publish/defaults.js +52 -0
- package/dist/publish/manifest.d.ts +27 -0
- package/dist/publish/manifest.js +81 -0
- package/dist/publish/paths.d.ts +25 -0
- package/dist/publish/paths.js +78 -0
- package/dist/publish/progress.d.ts +17 -0
- package/dist/publish/progress.js +45 -0
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +31 -0
- package/dist/publish/qa.f4.fixtures.d.ts +25 -0
- package/dist/publish/qa.f4.fixtures.js +140 -0
- package/dist/publish/upload.d.ts +30 -0
- package/dist/publish/upload.js +93 -0
- package/dist/relay/server.d.ts +32 -0
- package/dist/relay/server.js +80 -12
- package/dist/relay/store.d.ts +32 -3
- package/dist/relay/store.js +76 -22
- package/dist/relay/testers.d.ts +90 -0
- package/dist/relay/testers.js +144 -0
- package/dist/shared/authError.d.ts +1 -0
- package/dist/shared/authError.js +11 -0
- package/dist/shared/config.d.ts +73 -5
- package/dist/shared/config.js +133 -20
- package/package.json +2 -3
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
package/dist/lib/lifecycle.js
CHANGED
|
@@ -3,119 +3,107 @@
|
|
|
3
3
|
// lower-level pieces (backend calls, scaffold, preview, diagnostics); no CLI or
|
|
4
4
|
// MCP SDK concerns leak in here. Both surfaces render the returned data their
|
|
5
5
|
// own way.
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { getMe } from "./backend.js";
|
|
7
|
+
import { createCommand } from "./projects.js";
|
|
8
|
+
import { publishCommand } from "../publish/command.js";
|
|
9
|
+
import { activeOrgSlug, resolveActiveOrg } from "./org.js";
|
|
9
10
|
import { filterErrors, httpRelayClient, renderResult, } from "./diagnostics.js";
|
|
10
|
-
import { clearRelayInfo, readRelayInfo, writeRelayInfo } from "./relayInfo.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
11
|
+
import { clearRelayInfo, readLivePreview, readRelayInfo, writeRelayInfo, } from "./relayInfo.js";
|
|
12
|
+
import { loadMachineConfig, locateProjectBinding, resolveConfig, } from "../shared/config.js";
|
|
13
|
+
import { startPreview, } from "../preview/index.js";
|
|
14
|
+
/**
|
|
15
|
+
* The shared `create` orchestration behind both `vincentt create` and the MCP
|
|
16
|
+
* `project_create` tool, so the two surfaces can never diverge in behavior OR in
|
|
17
|
+
* what they tell the caller.
|
|
18
|
+
*
|
|
19
|
+
* It does NOT scaffold — that is `vincentt init`, which needs no account. See
|
|
20
|
+
* `initCommand`.
|
|
21
|
+
*/
|
|
14
22
|
export async function projectCreate(projectCwd, opts = {}) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const cfg = await resolveConfig(projectCwd);
|
|
30
|
-
let scaffolded = false;
|
|
31
|
-
if (opts.scaffold !== false && (await needsScaffold(projectCwd))) {
|
|
32
|
-
progress("Scaffolding the v2-template starter…");
|
|
33
|
-
await scaffoldFromTemplate(projectCwd);
|
|
34
|
-
scaffolded = true;
|
|
35
|
-
}
|
|
36
|
-
const name = opts.name ?? path.basename(projectCwd);
|
|
37
|
-
progress("Registering the project…");
|
|
38
|
-
const created = await createProjectPreferringSlug(cfg, name, opts.slug);
|
|
39
|
-
const bindingPath = await writeProjectBinding(projectCwd, {
|
|
40
|
-
projectId: created.projectId,
|
|
41
|
-
slug: created.slug,
|
|
42
|
-
});
|
|
43
|
-
const steps = [];
|
|
44
|
-
if (scaffolded) {
|
|
45
|
-
steps.push("Scaffolded v2-template into this directory.");
|
|
46
|
-
const pm = await detectPackageManager(projectCwd);
|
|
47
|
-
progress(`Installing dependencies (${pm} install — this can take a minute)…`);
|
|
48
|
-
try {
|
|
49
|
-
await installDependencies(projectCwd);
|
|
50
|
-
steps.push(`Installed dependencies (${pm}).`);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
54
|
-
steps.push(`Dependencies NOT installed (${msg}) — run \`${pm} install\` before preview.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const preamble = steps.length ? steps.map((s) => `✓ ${s}`).join("\n") + "\n" : "";
|
|
58
|
-
return {
|
|
59
|
-
message: preamble +
|
|
60
|
-
`✓ Created "${created.name}" — slug: ${created.slug} (id ${created.projectId})\n` +
|
|
61
|
-
` Live URL after publish: https://${created.slug}.vincentt.app\n` +
|
|
62
|
-
` Binding: ${bindingPath} (gitignored)\n\n` +
|
|
63
|
-
`Next:\n` +
|
|
64
|
-
` vincentt preview open a live URL on your phone\n` +
|
|
65
|
-
` vincentt publish go live at ${created.slug}.vincentt.app`,
|
|
66
|
-
alreadyBound: false,
|
|
67
|
-
slug: created.slug,
|
|
68
|
-
projectId: created.projectId,
|
|
23
|
+
const lines = [];
|
|
24
|
+
const print = (line) => {
|
|
25
|
+
lines.push(line);
|
|
26
|
+
opts.onLine?.(line);
|
|
27
|
+
};
|
|
28
|
+
// Resolved once, lazily: a create refused by a local guard must not require a
|
|
29
|
+
// PAT and must not reach the network at all.
|
|
30
|
+
let cached;
|
|
31
|
+
const config = async () => {
|
|
32
|
+
cached ??= await resolveConfig(projectCwd, {
|
|
33
|
+
allowCustomApi: opts.allowCustomApi,
|
|
34
|
+
warn: print,
|
|
35
|
+
});
|
|
36
|
+
return cached;
|
|
69
37
|
};
|
|
38
|
+
const exitCode = await createCommand({
|
|
39
|
+
print,
|
|
40
|
+
cwd: projectCwd,
|
|
41
|
+
config,
|
|
42
|
+
org: async () => resolveOrgForCommand(await config(), { flag: opts.org }),
|
|
43
|
+
}, { name: opts.name, slug: opts.slug, force: opts.force });
|
|
44
|
+
return { message: lines.join("\n"), exitCode };
|
|
70
45
|
}
|
|
71
46
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* slug rather than failing create over a name we chose.
|
|
47
|
+
* Resolve which org a create acts in: the named one if it is an ACTIVE membership,
|
|
48
|
+
* else the personal org when nothing is named. A named org the caller is not an
|
|
49
|
+
* active member of resolves to nothing rather than silently falling back — acting
|
|
50
|
+
* in an org the creator did not ask for is the retargeting the binding rules forbid.
|
|
77
51
|
*/
|
|
78
|
-
async function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
52
|
+
export async function resolveOrgForCommand(cfg, input) {
|
|
53
|
+
const machine = await loadMachineConfig();
|
|
54
|
+
const named = {
|
|
55
|
+
flag: input.flag,
|
|
56
|
+
env: process.env.VINCENTT_ORG,
|
|
57
|
+
config: machine.activeOrg,
|
|
58
|
+
};
|
|
59
|
+
const me = await getMe(cfg);
|
|
60
|
+
const org = resolveActiveOrg(me, named);
|
|
61
|
+
if (org)
|
|
62
|
+
return { ok: true, org };
|
|
63
|
+
const asked = activeOrgSlug(named);
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
message: asked
|
|
67
|
+
? `✗ You are not an active member of "${asked.slug}".\n\n Run \`vincentt whoami\` to see which organizations you belong to.`
|
|
68
|
+
: "✗ No active organization.\n\n Run `vincentt whoami` to see which organizations you belong to,\n then re-run with `--org <slug>`.",
|
|
69
|
+
};
|
|
96
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The shared `publish` orchestration behind both `vincentt publish` and the MCP
|
|
73
|
+
* `project_publish` tool, so the two surfaces can never diverge.
|
|
74
|
+
*
|
|
75
|
+
* MCP has no terminal, so it is a non-TTY caller: the command refuses without
|
|
76
|
+
* `--yes` rather than assuming consent, and refuses `--yes` outright on a first
|
|
77
|
+
* publish. An agent cannot make an address permanent.
|
|
78
|
+
*/
|
|
97
79
|
export async function projectPublish(projectCwd, opts = {}) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
message: "No project is bound to this directory. Run `vincentt create` first.",
|
|
102
|
-
notBound: true,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const cfg = await resolveConfig(projectCwd);
|
|
106
|
-
const dist = path.resolve(projectCwd, opts.distDir ?? "dist");
|
|
107
|
-
const commitSha = await gitHeadSha(projectCwd);
|
|
108
|
-
const result = await publishUpload(cfg, binding.projectId, dist, {
|
|
109
|
-
note: opts.note,
|
|
110
|
-
commitSha,
|
|
111
|
-
});
|
|
112
|
-
return {
|
|
113
|
-
message: `Published v${result.version}. Live at ${result.liveUrl}`,
|
|
114
|
-
notBound: false,
|
|
115
|
-
version: result.version,
|
|
116
|
-
liveUrl: result.liveUrl,
|
|
117
|
-
url: result.url,
|
|
80
|
+
const lines = [];
|
|
81
|
+
const print = (line) => {
|
|
82
|
+
lines.push(line);
|
|
118
83
|
};
|
|
84
|
+
const exitCode = await publishCommand({
|
|
85
|
+
print,
|
|
86
|
+
write: () => {
|
|
87
|
+
// No redraw off a terminal; the degraded path emits whole lines.
|
|
88
|
+
},
|
|
89
|
+
confirm: async () => false,
|
|
90
|
+
cwd: projectCwd,
|
|
91
|
+
config: () => resolveConfig(projectCwd, {
|
|
92
|
+
allowCustomApi: opts.allowCustomApi,
|
|
93
|
+
warn: opts.warn ?? print,
|
|
94
|
+
}),
|
|
95
|
+
binding: async () => {
|
|
96
|
+
const located = await locateProjectBinding(projectCwd);
|
|
97
|
+
return located
|
|
98
|
+
? { projectId: located.binding.projectId, slug: located.binding.slug }
|
|
99
|
+
: undefined;
|
|
100
|
+
},
|
|
101
|
+
token: async () => process.env.VINCENTT_PAT ?? (await loadMachineConfig()).pat,
|
|
102
|
+
isTTY: false,
|
|
103
|
+
now: () => Date.now(),
|
|
104
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
105
|
+
}, { distDir: opts.distDir, note: opts.note, yes: opts.yes });
|
|
106
|
+
return { message: lines.join("\n"), exitCode };
|
|
119
107
|
}
|
|
120
108
|
// ---- preview (one per process) ----------------------------------------------
|
|
121
109
|
let activePreview = null;
|
|
@@ -123,10 +111,28 @@ let activeProjectCwd = null;
|
|
|
123
111
|
export function getActivePreview() {
|
|
124
112
|
return activePreview;
|
|
125
113
|
}
|
|
126
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the binding and the config, then start. The binding is a HINT and
|
|
116
|
+
* never an authority (B12): the projectId it carries is presented to the API,
|
|
117
|
+
* which resolves the org itself and answers 404 for every unreachable cause.
|
|
118
|
+
*/
|
|
119
|
+
export async function previewStart(projectCwd, onLog, extra = {}) {
|
|
127
120
|
if (activePreview)
|
|
128
121
|
return { preview: activePreview, started: false };
|
|
129
|
-
const
|
|
122
|
+
const located = await locateProjectBinding(projectCwd);
|
|
123
|
+
if (!located) {
|
|
124
|
+
throw new Error("This folder isn't linked to a project.\n\n" +
|
|
125
|
+
" vincentt link <project-id> (copy it from the console)\n" +
|
|
126
|
+
" vincentt create Register a new project for this folder");
|
|
127
|
+
}
|
|
128
|
+
const config = await resolveConfig(projectCwd, { warn: onLog });
|
|
129
|
+
const preview = await startPreview({
|
|
130
|
+
...extra,
|
|
131
|
+
projectCwd,
|
|
132
|
+
config,
|
|
133
|
+
projectId: located.binding.projectId,
|
|
134
|
+
onLog,
|
|
135
|
+
});
|
|
130
136
|
activePreview = preview;
|
|
131
137
|
activeProjectCwd = projectCwd;
|
|
132
138
|
// Record the OS-assigned relay port so a separate `vincentt logs`/`feedback`
|
|
@@ -135,7 +141,6 @@ export async function previewStart(projectCwd, onLog) {
|
|
|
135
141
|
relayPort: preview.relayPort,
|
|
136
142
|
url: preview.url,
|
|
137
143
|
pid: process.pid,
|
|
138
|
-
tunnelPid: preview.tunnelPid,
|
|
139
144
|
harnessPath: "/__harness",
|
|
140
145
|
});
|
|
141
146
|
return { preview, started: true };
|
|
@@ -164,11 +169,14 @@ function isAlive(pid) {
|
|
|
164
169
|
/**
|
|
165
170
|
* Stop a preview started by a *different* process (the common case: `vincentt
|
|
166
171
|
* preview` runs in the background and the agent later wants it down). Reads the
|
|
167
|
-
* pidfile and signals the recorded
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* the
|
|
172
|
+
* pidfile and signals the recorded pid rather than the in-process
|
|
173
|
+
* `activePreview`.
|
|
174
|
+
*
|
|
175
|
+
* There is NO orphaned-tunnel case any more, and that is the point of the edge
|
|
176
|
+
* repoint: the tunnel is a socket this process owns rather than a child process
|
|
177
|
+
* that could be reparented to init. When the preview process dies the socket
|
|
178
|
+
* closes, the edge sees the tunnel go, and the session drains on its own — so
|
|
179
|
+
* SIGTERM plus clearing the stale relay info is the whole of the cleanup.
|
|
172
180
|
*/
|
|
173
181
|
export async function previewStopSignal(projectCwd) {
|
|
174
182
|
const info = await readRelayInfo(projectCwd);
|
|
@@ -176,36 +184,30 @@ export async function previewStopSignal(projectCwd) {
|
|
|
176
184
|
return { stopped: false, message: "No preview is running for this directory." };
|
|
177
185
|
}
|
|
178
186
|
const previewAlive = info.pid !== undefined && isAlive(info.pid);
|
|
179
|
-
const reaped = [];
|
|
180
187
|
if (previewAlive) {
|
|
181
188
|
try {
|
|
182
189
|
process.kill(info.pid, "SIGTERM");
|
|
183
|
-
|
|
190
|
+
await clearRelayInfo(projectCwd);
|
|
191
|
+
return { stopped: true, message: "✓ Preview stopped. The link no longer works." };
|
|
184
192
|
}
|
|
185
193
|
catch {
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// Only force-kill the tunnel directly when no live preview process will reap it.
|
|
190
|
-
if (!previewAlive && info.tunnelPid !== undefined && isAlive(info.tunnelPid)) {
|
|
191
|
-
try {
|
|
192
|
-
process.kill(info.tunnelPid, "SIGKILL");
|
|
193
|
-
reaped.push("orphaned tunnel");
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
/* already gone */
|
|
194
|
+
// Raced with its own exit; the socket is gone either way.
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
197
|
await clearRelayInfo(projectCwd);
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
return {
|
|
199
|
+
stopped: false,
|
|
200
|
+
message: "No live preview process found; cleared stale relay info.",
|
|
201
|
+
};
|
|
203
202
|
}
|
|
204
203
|
/** Resolve the relay client for a project's running preview, or throw a hint. */
|
|
205
204
|
async function relayForProject(projectCwd, override) {
|
|
206
205
|
if (override)
|
|
207
206
|
return override;
|
|
208
|
-
|
|
207
|
+
// VALIDATED, for the same reason `vincentt preview` validates it: a stale
|
|
208
|
+
// marker would point these reads at a dead port and surface a connection
|
|
209
|
+
// error instead of the one sentence that tells the creator what to do.
|
|
210
|
+
const info = await readLivePreview(projectCwd);
|
|
209
211
|
if (!info) {
|
|
210
212
|
throw new Error("No running preview for this project — run `vincentt preview` first.");
|
|
211
213
|
}
|
|
@@ -216,9 +218,10 @@ export async function diagQuery(projectCwd, kind, opts = {}, relay) {
|
|
|
216
218
|
const client = await relayForProject(projectCwd, relay);
|
|
217
219
|
const q = {
|
|
218
220
|
kind,
|
|
219
|
-
|
|
221
|
+
tester: opts.tester,
|
|
220
222
|
since: opts.since,
|
|
221
223
|
limit: opts.limit,
|
|
224
|
+
viewer: opts.viewer,
|
|
222
225
|
};
|
|
223
226
|
const result = await client.query(q);
|
|
224
227
|
return kind === "log" && opts.errorsOnly ? filterErrors(result) : result;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Me } from "./backend.js";
|
|
2
|
+
export type OrgSource = "flag" | "env" | "config";
|
|
3
|
+
export interface ActiveOrgInput {
|
|
4
|
+
/** --org <slug> */
|
|
5
|
+
flag?: string;
|
|
6
|
+
/** VINCENTT_ORG */
|
|
7
|
+
env?: string;
|
|
8
|
+
/** ~/.vincentt/config.json activeOrg */
|
|
9
|
+
config?: string;
|
|
10
|
+
}
|
|
11
|
+
/** flag → env → machine config. Nothing else; no inference, no folder-derived default. */
|
|
12
|
+
export declare function activeOrgSlug(input: ActiveOrgInput): {
|
|
13
|
+
slug: string;
|
|
14
|
+
source: OrgSource;
|
|
15
|
+
} | undefined;
|
|
16
|
+
export interface ResolvedOrg {
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
kind: "personal" | "team";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pair a resolved slug with the membership `/me` reports for it. A slug that
|
|
23
|
+
* names no membership resolves to NOTHING rather than to a plausible default —
|
|
24
|
+
* acting in an org the creator did not name is silent retargeting.
|
|
25
|
+
*
|
|
26
|
+
* With no slug configured at all, the personal org is the fallback, matching what
|
|
27
|
+
* `vincentt login` already prints as the active org on a fresh machine.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveActiveOrg(me: Me, input: ActiveOrgInput): ResolvedOrg | undefined;
|
package/dist/lib/org.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Which org does a command act in? Resolved by PRECEDENCE, from sources the person
|
|
2
|
+
// running the command controls — never from the folder's binding.
|
|
3
|
+
//
|
|
4
|
+
// `whoami` answers "who am I", so reading the binding would make it answer a
|
|
5
|
+
// question about the folder instead, which is precisely the mystery a creator
|
|
6
|
+
// running it is trying to escape.
|
|
7
|
+
/** flag → env → machine config. Nothing else; no inference, no folder-derived default. */
|
|
8
|
+
export function activeOrgSlug(input) {
|
|
9
|
+
if (input.flag)
|
|
10
|
+
return { slug: input.flag, source: "flag" };
|
|
11
|
+
if (input.env)
|
|
12
|
+
return { slug: input.env, source: "env" };
|
|
13
|
+
if (input.config)
|
|
14
|
+
return { slug: input.config, source: "config" };
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pair a resolved slug with the membership `/me` reports for it. A slug that
|
|
19
|
+
* names no membership resolves to NOTHING rather than to a plausible default —
|
|
20
|
+
* acting in an org the creator did not name is silent retargeting.
|
|
21
|
+
*
|
|
22
|
+
* With no slug configured at all, the personal org is the fallback, matching what
|
|
23
|
+
* `vincentt login` already prints as the active org on a fresh machine.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveActiveOrg(me, input) {
|
|
26
|
+
const memberships = (me.memberships ?? []).filter(isActive);
|
|
27
|
+
const named = activeOrgSlug(input);
|
|
28
|
+
if (named) {
|
|
29
|
+
const match = memberships.find((m) => m.orgSlug === named.slug);
|
|
30
|
+
return match ? toOrg(match) : undefined;
|
|
31
|
+
}
|
|
32
|
+
const personal = memberships.find((m) => m.orgKind === "personal");
|
|
33
|
+
return personal ? toOrg(personal) : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An invited-but-never-accepted membership carries its INTENDED role from the
|
|
37
|
+
* moment the invite is sent, so a bare role read would treat a pending invite as
|
|
38
|
+
* a real one. Only active rows are memberships as far as the CLI is concerned.
|
|
39
|
+
*/
|
|
40
|
+
function isActive(m) {
|
|
41
|
+
return m.status === "active";
|
|
42
|
+
}
|
|
43
|
+
function toOrg(m) {
|
|
44
|
+
return { slug: m.orgSlug, name: m.orgName, kind: m.orgKind };
|
|
45
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
export interface ProjectCommandDeps {
|
|
3
|
+
/** Print a transcript line. */
|
|
4
|
+
print: (line: string) => void;
|
|
5
|
+
/** The directory the command acts in. */
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** Resolve the API host + PAT (the apiUrl gate lives here). */
|
|
8
|
+
config: () => Promise<ResolvedConfig>;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateCommandDeps extends ProjectCommandDeps {
|
|
11
|
+
/**
|
|
12
|
+
* The org to create in, resolved LAZILY: every local guard (already linked,
|
|
13
|
+
* unusable name) runs before this is called, so a refused create makes no
|
|
14
|
+
* server request at all.
|
|
15
|
+
*/
|
|
16
|
+
org: () => Promise<{
|
|
17
|
+
ok: true;
|
|
18
|
+
org: {
|
|
19
|
+
slug: string;
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export interface CreateCommandOptions {
|
|
28
|
+
name?: string;
|
|
29
|
+
slug?: string;
|
|
30
|
+
/** Replace a binding this folder already has. */
|
|
31
|
+
force?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `vincentt create` — register a project and link this folder to it.
|
|
35
|
+
*
|
|
36
|
+
* It does NOT scaffold. An unscaffolded folder gets a `!` advisory and the command
|
|
37
|
+
* PROCEEDS: the server accepts a create from any folder or none, and blocking here
|
|
38
|
+
* would put the open template behind the auth gate, which is the one thing the
|
|
39
|
+
* init/create split exists to prevent.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createCommand(deps: CreateCommandDeps, opts?: CreateCommandOptions): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* `vincentt link <project-id>` — one verb for four situations (console-first
|
|
44
|
+
* birth, a fresh clone, a folder that lost its binding, a re-link after
|
|
45
|
+
* `login --force`). It is a READ plus a local write: the server has no notion of
|
|
46
|
+
* "which folder", so linking is entirely client-side, gated only by the server's
|
|
47
|
+
* willingness to return the project at all.
|
|
48
|
+
*/
|
|
49
|
+
export declare function linkCommand(deps: ProjectCommandDeps, projectId: string | undefined): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* The API's error messages are lowercase and unpunctuated by convention
|
|
52
|
+
* (`that handle is taken`, `this project is not available to you`), while both
|
|
53
|
+
* client surfaces present them as sentences. Casing is the ONLY thing either
|
|
54
|
+
* surface may change — the words are the server's, so a creator moving between
|
|
55
|
+
* the CLI and the console reads one sentence and support can match a screenshot
|
|
56
|
+
* to a log line without a translation table.
|
|
57
|
+
*/
|
|
58
|
+
export declare function asSentence(message: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Flow L10. Line 1 is the API's message; the body names both candidate causes and
|
|
61
|
+
* resolves neither (that is the maximum the anti-oracle contract permits, and it
|
|
62
|
+
* is accurate for all four real causes). `whoami` is offered first because it is
|
|
63
|
+
* the one diagnosis that discloses nothing about the project.
|
|
64
|
+
*/
|
|
65
|
+
export declare function printUnreachable(print: (line: string) => void, message: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* `vincentt unlink` — deletes the local binding. NO server call: the binding is a
|
|
68
|
+
* local pointer and the project is untouched, so there is nothing to tell the
|
|
69
|
+
* server about.
|
|
70
|
+
*/
|
|
71
|
+
export declare function unlinkCommand(deps: ProjectCommandDeps): Promise<number>;
|
|
72
|
+
/**
|
|
73
|
+
* The advisory's trigger: a package.json that depends on @vincentt-xr/sdk. It is a
|
|
74
|
+
* HINT only — create proceeds either way — so a false negative costs one extra
|
|
75
|
+
* line of output, never a refused command.
|
|
76
|
+
*/
|
|
77
|
+
export declare function looksLikeVincenttApp(cwd: string): Promise<boolean>;
|
|
78
|
+
/** Resolve the bound project for a project-scoped verb, or explain why not. */
|
|
79
|
+
export declare function requireBinding(deps: ProjectCommandDeps): Promise<{
|
|
80
|
+
projectId: string;
|
|
81
|
+
slug: string;
|
|
82
|
+
} | undefined>;
|