@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.
Files changed (92) hide show
  1. package/README.md +94 -0
  2. package/dist/cli/args.d.ts +14 -0
  3. package/dist/cli/args.js +51 -0
  4. package/dist/cli/index.js +416 -100
  5. package/dist/lib/backend.d.ts +44 -20
  6. package/dist/lib/backend.js +98 -51
  7. package/dist/lib/diagnostics.d.ts +27 -2
  8. package/dist/lib/diagnostics.js +73 -8
  9. package/dist/lib/initCommand.d.ts +14 -0
  10. package/dist/lib/initCommand.js +75 -0
  11. package/dist/lib/lifecycle.d.ts +85 -19
  12. package/dist/lib/lifecycle.js +133 -130
  13. package/dist/lib/org.d.ts +29 -0
  14. package/dist/lib/org.js +45 -0
  15. package/dist/lib/projects.d.ts +82 -0
  16. package/dist/lib/projects.js +217 -0
  17. package/dist/lib/relayInfo.d.ts +41 -3
  18. package/dist/lib/relayInfo.js +94 -0
  19. package/dist/lib/whoami.d.ts +11 -0
  20. package/dist/lib/whoami.js +38 -0
  21. package/dist/login/authBackend.d.ts +30 -0
  22. package/dist/login/authBackend.js +80 -0
  23. package/dist/login/guard.d.ts +13 -0
  24. package/dist/login/guard.js +17 -0
  25. package/dist/login/loginCommand.d.ts +39 -0
  26. package/dist/login/loginCommand.js +72 -0
  27. package/dist/login/pkce.d.ts +47 -0
  28. package/dist/login/pkce.js +106 -0
  29. package/dist/login/pkceLogin.d.ts +53 -0
  30. package/dist/login/pkceLogin.js +152 -0
  31. package/dist/mcp/server.js +52 -25
  32. package/dist/preview/api.d.ts +84 -0
  33. package/dist/preview/api.js +174 -0
  34. package/dist/preview/command.d.ts +96 -0
  35. package/dist/preview/command.js +276 -0
  36. package/dist/preview/copy.d.ts +209 -0
  37. package/dist/preview/copy.js +444 -0
  38. package/dist/preview/index.d.ts +2 -3
  39. package/dist/preview/index.js +5 -6
  40. package/dist/preview/proxy.d.ts +12 -1
  41. package/dist/preview/proxy.js +17 -7
  42. package/dist/preview/qr.d.ts +30 -0
  43. package/dist/preview/qr.js +89 -0
  44. package/dist/preview/runner.d.ts +155 -30
  45. package/dist/preview/runner.js +219 -68
  46. package/dist/preview/stream.d.ts +52 -0
  47. package/dist/preview/stream.js +171 -0
  48. package/dist/preview/viewers.d.ts +113 -0
  49. package/dist/preview/viewers.js +242 -0
  50. package/dist/publish/api.d.ts +67 -0
  51. package/dist/publish/api.js +96 -0
  52. package/dist/publish/bounds.d.ts +25 -0
  53. package/dist/publish/bounds.js +53 -0
  54. package/dist/publish/classify.d.ts +51 -0
  55. package/dist/publish/classify.js +138 -0
  56. package/dist/publish/command.d.ts +27 -0
  57. package/dist/publish/command.js +328 -0
  58. package/dist/publish/consoleVerbs.d.ts +17 -0
  59. package/dist/publish/consoleVerbs.js +26 -0
  60. package/dist/publish/copy.d.ts +180 -0
  61. package/dist/publish/copy.js +606 -0
  62. package/dist/publish/defaults.d.ts +16 -0
  63. package/dist/publish/defaults.js +52 -0
  64. package/dist/publish/manifest.d.ts +27 -0
  65. package/dist/publish/manifest.js +81 -0
  66. package/dist/publish/paths.d.ts +25 -0
  67. package/dist/publish/paths.js +78 -0
  68. package/dist/publish/progress.d.ts +17 -0
  69. package/dist/publish/progress.js +45 -0
  70. package/dist/publish/prompt.d.ts +4 -0
  71. package/dist/publish/prompt.js +31 -0
  72. package/dist/publish/qa.f4.fixtures.d.ts +25 -0
  73. package/dist/publish/qa.f4.fixtures.js +140 -0
  74. package/dist/publish/upload.d.ts +30 -0
  75. package/dist/publish/upload.js +93 -0
  76. package/dist/relay/server.d.ts +32 -0
  77. package/dist/relay/server.js +80 -12
  78. package/dist/relay/store.d.ts +32 -3
  79. package/dist/relay/store.js +76 -22
  80. package/dist/relay/testers.d.ts +90 -0
  81. package/dist/relay/testers.js +144 -0
  82. package/dist/shared/authError.d.ts +1 -0
  83. package/dist/shared/authError.js +11 -0
  84. package/dist/shared/config.d.ts +73 -5
  85. package/dist/shared/config.js +133 -20
  86. package/package.json +2 -3
  87. package/dist/login/login.d.ts +0 -34
  88. package/dist/login/login.js +0 -148
  89. package/dist/preview/cloudflared.d.ts +0 -13
  90. package/dist/preview/cloudflared.js +0 -46
  91. package/dist/preview/tunnel.d.ts +0 -14
  92. package/dist/preview/tunnel.js +0 -28
@@ -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 path from "node:path";
7
- import { BackendError, createProject, gitHeadSha, publishUpload } from "./backend.js";
8
- import { slugify } from "./slug.js";
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 { clearProjectBinding, loadProjectBinding, resolveConfig, writeProjectBinding, } from "../shared/config.js";
12
- import { detectPackageManager, installDependencies, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
13
- import { startPreview } from "../preview/index.js";
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 progress = opts.onProgress ?? (() => { });
16
- const existing = await loadProjectBinding(projectCwd);
17
- if (existing && !opts.force) {
18
- return {
19
- message: `This directory is already bound to project ${existing.projectId} (slug ${existing.slug}).\n` +
20
- `Run \`vincentt create --force\` to rebind, or \`vincentt unbind\` to detach.`,
21
- alreadyBound: true,
22
- slug: existing.slug,
23
- projectId: existing.projectId,
24
- };
25
- }
26
- if (existing)
27
- await clearProjectBinding(projectCwd);
28
- // Resolve config first so a missing PAT fails before any clone/side effect.
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
- * Create the backend project, preferring a slug derived from the name so the live
73
- * URL reads like the app (`my-app.vincentt.app`) instead of a random one. An
74
- * explicit `slug` is authoritative — a collision surfaces so the caller re-asks.
75
- * A derived slug is best-effort: on any 4xx we fall back to a platform-assigned
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 createProjectPreferringSlug(cfg, name, explicitSlug) {
79
- if (explicitSlug)
80
- return createProject(cfg, name, explicitSlug);
81
- const candidate = slugify(name);
82
- if (!candidate)
83
- return createProject(cfg, name);
84
- try {
85
- return await createProject(cfg, name, candidate);
86
- }
87
- catch (err) {
88
- if (err instanceof BackendError &&
89
- err.status !== undefined &&
90
- err.status >= 400 &&
91
- err.status < 500) {
92
- return createProject(cfg, name); // taken/invalid derived slug → let the platform pick
93
- }
94
- throw err;
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 binding = await loadProjectBinding(projectCwd);
99
- if (!binding) {
100
- return {
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
- export async function previewStart(projectCwd, onLog) {
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 preview = await startPreview({ projectCwd, onLog });
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 pids rather than the in-process `activePreview`.
168
- * SIGTERM to the preview process runs its graceful handler (which reaps the tunnel
169
- * + backend DNS route); if that process is already gone, its cloudflared child may
170
- * be orphaned to init, so kill the recorded tunnel pid directly. Either way, clear
171
- * the (now stale) relay info.
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
- reaped.push("preview process");
190
+ await clearRelayInfo(projectCwd);
191
+ return { stopped: true, message: "✓ Preview stopped. The link no longer works." };
184
192
  }
185
193
  catch {
186
- /* raced with its own exit — fall through to tunnel cleanup */
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 reaped.length > 0
201
- ? { stopped: true, message: `Stopped preview (${reaped.join(", ")}).` }
202
- : { stopped: false, message: "No live preview process found; cleared stale relay info." };
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
- const info = await readRelayInfo(projectCwd);
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
- sessionId: opts.sessionId,
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;
@@ -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>;