@vincentt-xr/cli 0.1.0 → 0.1.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/cli/index.js +21 -4
- package/dist/lib/backend.js +2 -2
- package/dist/lib/lifecycle.d.ts +4 -0
- package/dist/lib/lifecycle.js +54 -14
- package/dist/lib/slug.d.ts +1 -0
- package/dist/lib/slug.js +14 -0
- package/dist/preview/runner.d.ts +8 -3
- package/dist/preview/runner.js +22 -7
- package/dist/scaffold/index.d.ts +14 -5
- package/dist/scaffold/index.js +31 -6
- package/dist/shared/config.d.ts +5 -0
- package/dist/shared/config.js +14 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { runLogin } from "../login/login.js";
|
|
15
|
-
import { clearMachineToken, loadMachineConfig, } from "../shared/config.js";
|
|
15
|
+
import { clearMachineToken, clearProjectBinding, loadMachineConfig, } from "../shared/config.js";
|
|
16
16
|
import { getMe } from "../lib/backend.js";
|
|
17
17
|
import { diagQuery, diagRead, previewStart, previewStop, projectCreate, projectPublish, } from "../lib/lifecycle.js";
|
|
18
18
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
@@ -82,10 +82,23 @@ async function create() {
|
|
|
82
82
|
name: flag("--name"),
|
|
83
83
|
slug: flag("--slug"),
|
|
84
84
|
scaffold: has("--no-scaffold") ? false : undefined,
|
|
85
|
+
force: has("--force"),
|
|
86
|
+
onProgress: (m) => console.error(` ${m}`),
|
|
85
87
|
});
|
|
86
88
|
console.log(result.message);
|
|
87
|
-
if (result.alreadyBound)
|
|
89
|
+
if (result.alreadyBound) {
|
|
88
90
|
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Chain straight into the dev loop when asked.
|
|
94
|
+
if (has("--preview")) {
|
|
95
|
+
console.log("");
|
|
96
|
+
await preview();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function unbind() {
|
|
100
|
+
const { path: p, hadBinding } = await clearProjectBinding(projectDir());
|
|
101
|
+
console.log(hadBinding ? `✓ Unbound. Removed ${p}` : "This directory has no project binding.");
|
|
89
102
|
}
|
|
90
103
|
async function publish() {
|
|
91
104
|
const result = await projectPublish(projectDir(), {
|
|
@@ -182,9 +195,11 @@ function usage() {
|
|
|
182
195
|
`Auth\n` +
|
|
183
196
|
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
184
197
|
` logout Remove the saved access token\n` +
|
|
185
|
-
` whoami Show the signed-in account (via GET /me)\n\n` +
|
|
198
|
+
` whoami Show the signed-in account (via GET /network/me)\n\n` +
|
|
186
199
|
`Dev loop\n` +
|
|
187
|
-
` create
|
|
200
|
+
` create Scaffold + bind a project (empty dir → full starter)\n` +
|
|
201
|
+
` [--name <s>] [--slug <s>] [--preview] [--force]\n` +
|
|
202
|
+
` unbind Detach this directory from its project\n` +
|
|
188
203
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
189
204
|
` preview Serve + tunnel the app for on-device preview\n\n` +
|
|
190
205
|
`Diagnostics (need a running preview)\n` +
|
|
@@ -210,6 +225,8 @@ async function main() {
|
|
|
210
225
|
return whoami();
|
|
211
226
|
case "create":
|
|
212
227
|
return create();
|
|
228
|
+
case "unbind":
|
|
229
|
+
return unbind();
|
|
213
230
|
case "publish":
|
|
214
231
|
return publish();
|
|
215
232
|
case "preview":
|
package/dist/lib/backend.js
CHANGED
|
@@ -137,9 +137,9 @@ export async function publishUpload(cfg, projectId, distDir, opts = {}) {
|
|
|
137
137
|
/** Resolve the signed-in identity for `vincentt whoami`. Throws on any non-2xx so
|
|
138
138
|
* the caller can degrade to "token present, unverified". */
|
|
139
139
|
export async function getMe(cfg) {
|
|
140
|
-
const res = await authFetch(cfg, "/me");
|
|
140
|
+
const res = await authFetch(cfg, "/network/me");
|
|
141
141
|
if (!res.ok) {
|
|
142
|
-
throw new BackendError(`/me failed (${res.status}): ${await safeText(res)}`, res.status);
|
|
142
|
+
throw new BackendError(`/network/me failed (${res.status}): ${await safeText(res)}`, res.status);
|
|
143
143
|
}
|
|
144
144
|
return (await res.json());
|
|
145
145
|
}
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface CreateOptions {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
slug?: string;
|
|
7
7
|
scaffold?: boolean;
|
|
8
|
+
/** Rebind a directory that already has a binding (drops the old one). */
|
|
9
|
+
force?: boolean;
|
|
10
|
+
/** Step-level progress for slow phases (scaffold, install). CLI prints; MCP omits. */
|
|
11
|
+
onProgress?: (message: string) => void;
|
|
8
12
|
}
|
|
9
13
|
export interface CreateResult {
|
|
10
14
|
message: string;
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -4,56 +4,96 @@
|
|
|
4
4
|
// MCP SDK concerns leak in here. Both surfaces render the returned data their
|
|
5
5
|
// own way.
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { createProject, gitHeadSha, publishUpload } from "./backend.js";
|
|
7
|
+
import { BackendError, createProject, gitHeadSha, publishUpload } from "./backend.js";
|
|
8
|
+
import { slugify } from "./slug.js";
|
|
8
9
|
import { filterErrors, httpRelayClient, renderResult, } from "./diagnostics.js";
|
|
9
10
|
import { clearRelayInfo, readRelayInfo, writeRelayInfo } from "./relayInfo.js";
|
|
10
|
-
import { loadProjectBinding, resolveConfig, writeProjectBinding, } from "../shared/config.js";
|
|
11
|
-
import { installDependencies, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
11
|
+
import { clearProjectBinding, loadProjectBinding, resolveConfig, writeProjectBinding, } from "../shared/config.js";
|
|
12
|
+
import { detectPackageManager, installDependencies, needsScaffold, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
12
13
|
import { startPreview } from "../preview/index.js";
|
|
13
14
|
export async function projectCreate(projectCwd, opts = {}) {
|
|
15
|
+
const progress = opts.onProgress ?? (() => { });
|
|
14
16
|
const existing = await loadProjectBinding(projectCwd);
|
|
15
|
-
if (existing) {
|
|
17
|
+
if (existing && !opts.force) {
|
|
16
18
|
return {
|
|
17
|
-
message: `This directory is already bound to project ${existing.projectId} (slug ${existing.slug})
|
|
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.`,
|
|
18
21
|
alreadyBound: true,
|
|
19
22
|
slug: existing.slug,
|
|
20
23
|
projectId: existing.projectId,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
26
|
+
if (existing)
|
|
27
|
+
await clearProjectBinding(projectCwd);
|
|
23
28
|
// Resolve config first so a missing PAT fails before any clone/side effect.
|
|
24
29
|
const cfg = await resolveConfig(projectCwd);
|
|
25
30
|
let scaffolded = false;
|
|
26
31
|
if (opts.scaffold !== false && (await needsScaffold(projectCwd))) {
|
|
32
|
+
progress("Scaffolding the v2-template starter…");
|
|
27
33
|
await scaffoldFromTemplate(projectCwd);
|
|
28
34
|
scaffolded = true;
|
|
29
35
|
}
|
|
30
|
-
const
|
|
36
|
+
const name = opts.name ?? path.basename(projectCwd);
|
|
37
|
+
progress("Registering the project…");
|
|
38
|
+
const created = await createProjectPreferringSlug(cfg, name, opts.slug);
|
|
31
39
|
const bindingPath = await writeProjectBinding(projectCwd, {
|
|
32
40
|
projectId: created.projectId,
|
|
33
41
|
slug: created.slug,
|
|
34
42
|
});
|
|
35
|
-
|
|
43
|
+
const steps = [];
|
|
36
44
|
if (scaffolded) {
|
|
37
|
-
|
|
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)…`);
|
|
38
48
|
try {
|
|
39
49
|
await installDependencies(projectCwd);
|
|
40
|
-
|
|
50
|
+
steps.push(`Installed dependencies (${pm}).`);
|
|
41
51
|
}
|
|
42
52
|
catch (err) {
|
|
43
53
|
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
-
|
|
54
|
+
steps.push(`Dependencies NOT installed (${msg}) — run \`${pm} install\` before preview.`);
|
|
45
55
|
}
|
|
46
56
|
}
|
|
57
|
+
const preamble = steps.length ? steps.map((s) => `✓ ${s}`).join("\n") + "\n" : "";
|
|
47
58
|
return {
|
|
48
|
-
message:
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
`
|
|
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`,
|
|
52
66
|
alreadyBound: false,
|
|
53
67
|
slug: created.slug,
|
|
54
68
|
projectId: created.projectId,
|
|
55
69
|
};
|
|
56
70
|
}
|
|
71
|
+
/**
|
|
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.
|
|
77
|
+
*/
|
|
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
|
+
}
|
|
96
|
+
}
|
|
57
97
|
export async function projectPublish(projectCwd, opts = {}) {
|
|
58
98
|
const binding = await loadProjectBinding(projectCwd);
|
|
59
99
|
if (!binding) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function slugify(input: string): string;
|
package/dist/lib/slug.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Derive a DNS-safe slug candidate from a human name. The slug becomes the
|
|
2
|
+
// permanent <slug>.vincentt.app host, so it must be lowercase (DNS lowercases,
|
|
3
|
+
// S3 does not — the platform enforces lowercase end-to-end) and a single DNS
|
|
4
|
+
// label (<= 63 chars). Returns "" when the name has no usable characters, in
|
|
5
|
+
// which case the caller lets the backend auto-assign.
|
|
6
|
+
export function slugify(input) {
|
|
7
|
+
return input
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.trim()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-+|-+$/g, "")
|
|
12
|
+
.slice(0, 63)
|
|
13
|
+
.replace(/-+$/g, "");
|
|
14
|
+
}
|
package/dist/preview/runner.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface StartPreviewOptions {
|
|
|
16
16
|
/** stdio for the app dev serve. "ignore" (default) keeps an MCP server's stdout
|
|
17
17
|
* clean; a CLI can pass "inherit" to surface build output. */
|
|
18
18
|
appStdio?: "ignore" | "inherit";
|
|
19
|
-
/** How long to wait for cloudflared to register before giving up (default
|
|
19
|
+
/** How long to wait for cloudflared to register before giving up (default 90s).
|
|
20
|
+
* Generous because on a network that blocks the QUIC edge, cloudflared spends
|
|
21
|
+
* ~20-30s failing QUIC before it falls back to the http2 edge, which does work. */
|
|
20
22
|
registerTimeoutMs?: number;
|
|
21
23
|
/** How long to wait for the app to serve before returning anyway (default 8s).
|
|
22
24
|
* Kept short so an MCP tool call stays within strict clients' ~30s timeout;
|
|
@@ -40,6 +42,9 @@ export interface RunningPreview {
|
|
|
40
42
|
export declare function startPreview(opts: StartPreviewOptions): Promise<RunningPreview>;
|
|
41
43
|
/**
|
|
42
44
|
* Resolve once cloudflared reports a live edge connection, reject if it exits
|
|
43
|
-
* first or never registers.
|
|
45
|
+
* first or never registers. On failure the rejection carries cloudflared's own
|
|
46
|
+
* recent output — otherwise "did not register" hides the real cause (an invalid
|
|
47
|
+
* run token, or a network that blocks the QUIC edge, the two common culprits).
|
|
48
|
+
* `onLine` streams each log line out live. Exported for unit testing.
|
|
44
49
|
*/
|
|
45
|
-
export declare function waitForRegister(tunnel: ChildProcess, timeoutMs: number): Promise<void>;
|
|
50
|
+
export declare function waitForRegister(tunnel: ChildProcess, timeoutMs: number, onLine?: (line: string) => void): Promise<void>;
|
package/dist/preview/runner.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startSessionTunnel } from "./tunnel.js";
|
|
|
16
16
|
import { proxyWeb, proxyWs } from "./proxy.js";
|
|
17
17
|
import { getFreePort, isPortListening, waitForApp } from "./net.js";
|
|
18
18
|
export async function startPreview(opts) {
|
|
19
|
-
const { projectCwd, harnessPath = "/__harness", devCommand = ["npm", "run", "dev"], appStdio = "ignore", registerTimeoutMs =
|
|
19
|
+
const { projectCwd, harnessPath = "/__harness", devCommand = ["npm", "run", "dev"], appStdio = "ignore", registerTimeoutMs = 90_000, appReadyTimeoutMs = 8_000, onLog = () => undefined, } = opts;
|
|
20
20
|
// Resolve ports. The app port defaults to 5173; the relay + front ports are
|
|
21
21
|
// harness-internal, so auto-pick free ones (the relay port is returned for the
|
|
22
22
|
// MCP diag_* tools to point at) instead of colliding on fixed defaults.
|
|
@@ -70,7 +70,7 @@ export async function startPreview(opts) {
|
|
|
70
70
|
tunnel = spawn(cloudflaredBin, ["tunnel", "run", "--token", session.runToken], {
|
|
71
71
|
stdio: ["ignore", "pipe", "pipe"],
|
|
72
72
|
});
|
|
73
|
-
await waitForRegister(tunnel, registerTimeoutMs);
|
|
73
|
+
await waitForRegister(tunnel, registerTimeoutMs, (line) => onLog(`[cloudflared] ${line}`));
|
|
74
74
|
// Don't hand back a live URL that serves a blank page: wait for the app to
|
|
75
75
|
// actually respond. Non-fatal — a slow build still returns, flagged not-ready.
|
|
76
76
|
const appReady = reuseApp ? true : await waitForApp(appPort, appReadyTimeoutMs);
|
|
@@ -86,11 +86,15 @@ export async function startPreview(opts) {
|
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Resolve once cloudflared reports a live edge connection, reject if it exits
|
|
89
|
-
* first or never registers.
|
|
89
|
+
* first or never registers. On failure the rejection carries cloudflared's own
|
|
90
|
+
* recent output — otherwise "did not register" hides the real cause (an invalid
|
|
91
|
+
* run token, or a network that blocks the QUIC edge, the two common culprits).
|
|
92
|
+
* `onLine` streams each log line out live. Exported for unit testing.
|
|
90
93
|
*/
|
|
91
|
-
export function waitForRegister(tunnel, timeoutMs) {
|
|
94
|
+
export function waitForRegister(tunnel, timeoutMs, onLine) {
|
|
92
95
|
return new Promise((resolve, reject) => {
|
|
93
96
|
let settled = false;
|
|
97
|
+
const recent = [];
|
|
94
98
|
const finish = (fn) => {
|
|
95
99
|
if (settled)
|
|
96
100
|
return;
|
|
@@ -98,14 +102,25 @@ export function waitForRegister(tunnel, timeoutMs) {
|
|
|
98
102
|
clearTimeout(timer);
|
|
99
103
|
fn();
|
|
100
104
|
};
|
|
105
|
+
const tail = () => {
|
|
106
|
+
const lines = recent.slice(-15).join("\n").trim();
|
|
107
|
+
return lines ? `\ncloudflared output:\n${lines}` : "";
|
|
108
|
+
};
|
|
101
109
|
const scan = (buf) => {
|
|
102
|
-
|
|
110
|
+
const text = String(buf);
|
|
111
|
+
for (const line of text.split(/\r?\n/)) {
|
|
112
|
+
if (!line.trim())
|
|
113
|
+
continue;
|
|
114
|
+
recent.push(line);
|
|
115
|
+
onLine?.(line);
|
|
116
|
+
}
|
|
117
|
+
if (/Registered tunnel connection|Connection [^ ]+ registered/.test(text)) {
|
|
103
118
|
finish(resolve);
|
|
104
119
|
}
|
|
105
120
|
};
|
|
106
121
|
tunnel.stdout?.on("data", scan);
|
|
107
122
|
tunnel.stderr?.on("data", scan);
|
|
108
|
-
tunnel.on("exit", (code) => finish(() => reject(new Error(`cloudflared exited before registering (code ${code})
|
|
109
|
-
const timer = setTimeout(() => finish(() => reject(new Error(`cloudflared did not register within ${timeoutMs}ms
|
|
123
|
+
tunnel.on("exit", (code) => finish(() => reject(new Error(`cloudflared exited before registering (code ${code}).${tail()}`))));
|
|
124
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`cloudflared did not register within ${timeoutMs}ms.${tail()}`))), timeoutMs);
|
|
110
125
|
});
|
|
111
126
|
}
|
package/dist/scaffold/index.d.ts
CHANGED
|
@@ -17,10 +17,19 @@ export interface ScaffoldOptions {
|
|
|
17
17
|
* fresh repo. Throws a clear error if the clone fails (e.g. no git access).
|
|
18
18
|
*/
|
|
19
19
|
export declare function scaffoldFromTemplate(targetDir: string, opts?: ScaffoldOptions): Promise<void>;
|
|
20
|
+
export type PackageManager = "pnpm" | "yarn" | "npm";
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
24
|
-
*
|
|
22
|
+
* Pick the package manager the scaffolded template is set up for, by its lockfile.
|
|
23
|
+
* The template ships a pnpm-lock.yaml + `node-linker=hoisted` (the hoisted layout
|
|
24
|
+
* the esbuild singleton aliasing needs), so installing with the wrong manager
|
|
25
|
+
* leaves a second lockfile and an unproven node_modules layout. Defaults to npm.
|
|
25
26
|
*/
|
|
26
|
-
export declare function
|
|
27
|
+
export declare function detectPackageManager(dir: string): Promise<PackageManager>;
|
|
28
|
+
/**
|
|
29
|
+
* Install a freshly scaffolded project's dependencies (with the template's own
|
|
30
|
+
* package manager) so the first preview/build doesn't fail on a missing
|
|
31
|
+
* node_modules. Throws on failure (no network, manager not installed); the caller
|
|
32
|
+
* surfaces it as a "run <pm> install yourself" hint rather than aborting create.
|
|
33
|
+
* Returns the manager used so the caller can name it in messages.
|
|
34
|
+
*/
|
|
35
|
+
export declare function installDependencies(dir: string): Promise<PackageManager>;
|
package/dist/scaffold/index.js
CHANGED
|
@@ -71,15 +71,40 @@ async function isGitRepo(dir) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the
|
|
77
|
-
*
|
|
74
|
+
* Pick the package manager the scaffolded template is set up for, by its lockfile.
|
|
75
|
+
* The template ships a pnpm-lock.yaml + `node-linker=hoisted` (the hoisted layout
|
|
76
|
+
* the esbuild singleton aliasing needs), so installing with the wrong manager
|
|
77
|
+
* leaves a second lockfile and an unproven node_modules layout. Defaults to npm.
|
|
78
|
+
*/
|
|
79
|
+
export async function detectPackageManager(dir) {
|
|
80
|
+
const has = async (f) => {
|
|
81
|
+
try {
|
|
82
|
+
await fs.access(path.join(dir, f));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
if (await has("pnpm-lock.yaml"))
|
|
90
|
+
return "pnpm";
|
|
91
|
+
if (await has("yarn.lock"))
|
|
92
|
+
return "yarn";
|
|
93
|
+
return "npm";
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Install a freshly scaffolded project's dependencies (with the template's own
|
|
97
|
+
* package manager) so the first preview/build doesn't fail on a missing
|
|
98
|
+
* node_modules. Throws on failure (no network, manager not installed); the caller
|
|
99
|
+
* surfaces it as a "run <pm> install yourself" hint rather than aborting create.
|
|
100
|
+
* Returns the manager used so the caller can name it in messages.
|
|
78
101
|
*/
|
|
79
102
|
export async function installDependencies(dir) {
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
const pm = await detectPackageManager(dir);
|
|
104
|
+
// Windows: the managers are .cmd shims — execFile needs a shell to resolve them.
|
|
105
|
+
await execFileAsync(pm, ["install"], {
|
|
82
106
|
cwd: dir,
|
|
83
107
|
shell: process.platform === "win32",
|
|
84
108
|
});
|
|
109
|
+
return pm;
|
|
85
110
|
}
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export declare function loadProjectBinding(cwd: string): Promise<ProjectBinding
|
|
|
35
35
|
* secret-free-but-tenant-scoped binding never rides a commit or `git archive`).
|
|
36
36
|
*/
|
|
37
37
|
export declare function writeProjectBinding(cwd: string, binding: ProjectBinding): Promise<string>;
|
|
38
|
+
/** Remove the per-tree binding so the directory can be re-created/rebound. */
|
|
39
|
+
export declare function clearProjectBinding(cwd: string): Promise<{
|
|
40
|
+
path: string;
|
|
41
|
+
hadBinding: boolean;
|
|
42
|
+
}>;
|
|
38
43
|
export interface ResolvedConfig {
|
|
39
44
|
apiUrl: string;
|
|
40
45
|
pat: string;
|
package/dist/shared/config.js
CHANGED
|
@@ -72,6 +72,13 @@ export async function writeProjectBinding(cwd, binding) {
|
|
|
72
72
|
await ensureGitignored(cwd, `${BINDING_DIR}/`);
|
|
73
73
|
return p;
|
|
74
74
|
}
|
|
75
|
+
/** Remove the per-tree binding so the directory can be re-created/rebound. */
|
|
76
|
+
export async function clearProjectBinding(cwd) {
|
|
77
|
+
const p = projectBindingPath(cwd);
|
|
78
|
+
const hadBinding = (await readJsonIfExists(p)) !== undefined;
|
|
79
|
+
await fs.rm(p, { force: true });
|
|
80
|
+
return { path: p, hadBinding };
|
|
81
|
+
}
|
|
75
82
|
/** Append a pattern to the repo's .gitignore if not already present (best-effort). */
|
|
76
83
|
async function ensureGitignored(cwd, pattern) {
|
|
77
84
|
const gitignore = path.join(cwd, ".gitignore");
|
|
@@ -80,7 +87,9 @@ async function ensureGitignored(cwd, pattern) {
|
|
|
80
87
|
const has = current.split(/\r?\n/).some((l) => l.trim() === pattern.trim());
|
|
81
88
|
if (has)
|
|
82
89
|
return;
|
|
83
|
-
const next = current && !current.endsWith("\n")
|
|
90
|
+
const next = current && !current.endsWith("\n")
|
|
91
|
+
? `${current}\n${pattern}\n`
|
|
92
|
+
: `${current}${pattern}\n`;
|
|
84
93
|
await fs.writeFile(gitignore, next, "utf8");
|
|
85
94
|
}
|
|
86
95
|
catch {
|
|
@@ -95,7 +104,10 @@ async function ensureGitignored(cwd, pattern) {
|
|
|
95
104
|
export async function resolveConfig(cwd) {
|
|
96
105
|
const machine = await loadMachineConfig();
|
|
97
106
|
const binding = await loadProjectBinding(cwd);
|
|
98
|
-
const apiUrl = binding?.apiUrl ??
|
|
107
|
+
const apiUrl = binding?.apiUrl ??
|
|
108
|
+
process.env.VINCENTT_API_URL ??
|
|
109
|
+
machine.apiUrl ??
|
|
110
|
+
"http://localhost:5051";
|
|
99
111
|
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
100
112
|
if (!pat) {
|
|
101
113
|
throw new Error(`No Vincentt access token found. Add {"apiUrl":"…","pat":"…"} to ${MACHINE_CONFIG_PATH}, ` +
|
package/package.json
CHANGED