@instafy/cli 0.1.8-staging.357 → 0.1.8-staging.364

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 CHANGED
@@ -9,6 +9,7 @@ Run Instafy projects locally and connect them back to Instafy Studio — from an
9
9
  0. Log in once: `instafy login`
10
10
  - Opens a Studio URL; the CLI continues automatically after you sign in.
11
11
  - Also enables Git auth (credential helper) for Instafy Git Service. Disable with `instafy login --no-git-setup`.
12
+ - Multiple accounts: `instafy login --profile work` (then bind folders with `instafy project:init --profile work` or `instafy project:profile work`).
12
13
  - Optional: set defaults with `instafy config set controller-url <url>` / `instafy config set studio-url <url>`
13
14
  1. Link a folder to a project:
14
15
  - VS Code: install the Instafy extension and run `Instafy: Link Workspace to Project`, or
package/dist/auth.js CHANGED
@@ -4,7 +4,7 @@ import * as http from "node:http";
4
4
  import { createRequire } from "node:module";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { stdin as input, stdout as output } from "node:process";
7
- import { clearInstafyCliConfig, getInstafyConfigPath, resolveConfiguredControllerUrl, resolveConfiguredStudioUrl, resolveUserAccessToken, writeInstafyCliConfig, } from "./config.js";
7
+ import { clearInstafyCliConfig, clearInstafyProfileConfig, getInstafyConfigPath, getInstafyProfileConfigPath, resolveConfiguredControllerUrl, resolveConfiguredStudioUrl, resolveUserAccessToken, writeInstafyCliConfig, writeInstafyProfileConfig, } from "./config.js";
8
8
  import { installGitCredentialHelper, uninstallGitCredentialHelper } from "./git-setup.js";
9
9
  const require = createRequire(import.meta.url);
10
10
  const cliVersion = (() => {
@@ -212,10 +212,11 @@ async function startCliLoginCallbackServer() {
212
212
  };
213
213
  }
214
214
  export async function login(options) {
215
+ const profile = typeof options.profile === "string" && options.profile.trim() ? options.profile.trim() : null;
215
216
  const explicitControllerUrl = normalizeUrl(options.controllerUrl ?? null) ??
216
217
  normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
217
218
  normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
218
- (isStagingCli ? null : resolveConfiguredControllerUrl());
219
+ (isStagingCli ? null : resolveConfiguredControllerUrl({ profile }));
219
220
  const defaultLocalControllerUrl = "http://127.0.0.1:8788";
220
221
  const defaultHostedControllerUrl = "https://controller.instafy.dev";
221
222
  const controllerUrl = explicitControllerUrl ??
@@ -226,12 +227,16 @@ export async function login(options) {
226
227
  : defaultHostedControllerUrl);
227
228
  const studioUrl = normalizeUrl(options.studioUrl ?? null) ??
228
229
  normalizeUrl(process.env["INSTAFY_STUDIO_URL"] ?? null) ??
229
- (isStagingCli ? null : resolveConfiguredStudioUrl()) ??
230
+ (isStagingCli ? null : resolveConfiguredStudioUrl({ profile })) ??
230
231
  deriveDefaultStudioUrl(controllerUrl);
231
232
  const url = new URL("/cli/login", studioUrl);
232
233
  url.searchParams.set("serverUrl", controllerUrl);
233
234
  if (options.json) {
234
- console.log(JSON.stringify({ url: url.toString(), configPath: getInstafyConfigPath() }));
235
+ console.log(JSON.stringify({
236
+ url: url.toString(),
237
+ profile,
238
+ configPath: profile ? getInstafyProfileConfigPath(profile) : getInstafyConfigPath(),
239
+ }));
235
240
  return;
236
241
  }
237
242
  let callbackServer = null;
@@ -259,7 +264,7 @@ export async function login(options) {
259
264
  console.log("2) After you sign in, copy the token shown on that page.");
260
265
  }
261
266
  console.log("");
262
- const existing = resolveUserAccessToken();
267
+ const existing = resolveUserAccessToken({ profile });
263
268
  let token = provided;
264
269
  if (!token && callbackServer) {
265
270
  if (input.isTTY) {
@@ -337,9 +342,14 @@ export async function login(options) {
337
342
  throw new Error("No token provided.");
338
343
  }
339
344
  if (!options.noStore) {
340
- writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
345
+ if (profile) {
346
+ writeInstafyProfileConfig(profile, { controllerUrl, studioUrl, accessToken: token });
347
+ }
348
+ else {
349
+ writeInstafyCliConfig({ controllerUrl, studioUrl, accessToken: token });
350
+ }
341
351
  console.log("");
342
- console.log(kleur.green(`Saved token to ${getInstafyConfigPath()}`));
352
+ console.log(kleur.green(`Saved token to ${profile ? getInstafyProfileConfigPath(profile) : getInstafyConfigPath()}`));
343
353
  if (options.gitSetup !== false) {
344
354
  try {
345
355
  const result = installGitCredentialHelper();
@@ -362,16 +372,26 @@ export async function login(options) {
362
372
  console.log(`- ${kleur.cyan("instafy runtime:start")}`);
363
373
  }
364
374
  export async function logout(options) {
365
- clearInstafyCliConfig(["accessToken"]);
366
- try {
367
- uninstallGitCredentialHelper();
375
+ if (options?.profile) {
376
+ clearInstafyProfileConfig(options.profile, ["accessToken"]);
368
377
  }
369
- catch {
370
- // ignore git helper cleanup failures
378
+ else {
379
+ clearInstafyCliConfig(["accessToken"]);
380
+ try {
381
+ uninstallGitCredentialHelper();
382
+ }
383
+ catch {
384
+ // ignore git helper cleanup failures
385
+ }
371
386
  }
372
387
  if (options?.json) {
373
388
  console.log(JSON.stringify({ ok: true }));
374
389
  return;
375
390
  }
376
- console.log(kleur.green("Logged out (cleared saved access token)."));
391
+ if (options?.profile) {
392
+ console.log(kleur.green(`Logged out (cleared saved access token for profile "${options.profile}").`));
393
+ }
394
+ else {
395
+ console.log(kleur.green("Logged out (cleared saved access token)."));
396
+ }
377
397
  }
package/dist/config.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { findProjectManifest } from "./project-manifest.js";
4
5
  const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
5
6
  const CONFIG_PATH = path.join(INSTAFY_DIR, "config.json");
7
+ const PROFILES_DIR = path.join(INSTAFY_DIR, "profiles");
6
8
  function normalizeToken(value) {
7
9
  if (typeof value !== "string") {
8
10
  return null;
@@ -24,9 +26,45 @@ function normalizeUrl(value) {
24
26
  }
25
27
  return trimmed.replace(/\/$/, "");
26
28
  }
29
+ function normalizeProfileName(value) {
30
+ const trimmed = typeof value === "string" ? value.trim() : "";
31
+ if (!trimmed) {
32
+ return null;
33
+ }
34
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(trimmed)) {
35
+ throw new Error(`Invalid profile name "${value}". Use letters/numbers plus . _ - (max 64 chars).`);
36
+ }
37
+ if (trimmed.includes("..")) {
38
+ throw new Error(`Invalid profile name "${value}".`);
39
+ }
40
+ return trimmed;
41
+ }
27
42
  export function getInstafyConfigPath() {
28
43
  return CONFIG_PATH;
29
44
  }
45
+ export function getInstafyProfilesDirPath() {
46
+ return PROFILES_DIR;
47
+ }
48
+ export function getInstafyProfileConfigPath(profile) {
49
+ const normalized = normalizeProfileName(profile);
50
+ if (!normalized) {
51
+ throw new Error("Profile name is required.");
52
+ }
53
+ return path.join(PROFILES_DIR, `${normalized}.json`);
54
+ }
55
+ export function listInstafyProfileNames() {
56
+ try {
57
+ const entries = fs.readdirSync(PROFILES_DIR, { withFileTypes: true });
58
+ return entries
59
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
60
+ .map((entry) => entry.name.slice(0, -".json".length))
61
+ .filter(Boolean)
62
+ .sort((a, b) => a.localeCompare(b));
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
30
68
  export function readInstafyCliConfig() {
31
69
  try {
32
70
  const raw = fs.readFileSync(CONFIG_PATH, "utf8");
@@ -46,6 +84,26 @@ export function readInstafyCliConfig() {
46
84
  return {};
47
85
  }
48
86
  }
87
+ export function readInstafyProfileConfig(profile) {
88
+ const filePath = getInstafyProfileConfigPath(profile);
89
+ try {
90
+ const raw = fs.readFileSync(filePath, "utf8");
91
+ const parsed = JSON.parse(raw);
92
+ if (!parsed || typeof parsed !== "object") {
93
+ return {};
94
+ }
95
+ const record = parsed;
96
+ return {
97
+ controllerUrl: normalizeUrl(typeof record.controllerUrl === "string" ? record.controllerUrl : null),
98
+ studioUrl: normalizeUrl(typeof record.studioUrl === "string" ? record.studioUrl : null),
99
+ accessToken: normalizeToken(typeof record.accessToken === "string" ? record.accessToken : null),
100
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
101
+ };
102
+ }
103
+ catch {
104
+ return {};
105
+ }
106
+ }
49
107
  export function writeInstafyCliConfig(update) {
50
108
  const existing = readInstafyCliConfig();
51
109
  const next = {
@@ -64,6 +122,25 @@ export function writeInstafyCliConfig(update) {
64
122
  }
65
123
  return next;
66
124
  }
125
+ export function writeInstafyProfileConfig(profile, update) {
126
+ const filePath = getInstafyProfileConfigPath(profile);
127
+ const existing = readInstafyProfileConfig(profile);
128
+ const next = {
129
+ controllerUrl: normalizeUrl(update.controllerUrl ?? existing.controllerUrl ?? null),
130
+ studioUrl: normalizeUrl(update.studioUrl ?? existing.studioUrl ?? null),
131
+ accessToken: normalizeToken(update.accessToken ?? existing.accessToken ?? null),
132
+ updatedAt: new Date().toISOString(),
133
+ };
134
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
135
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), { encoding: "utf8" });
136
+ try {
137
+ fs.chmodSync(filePath, 0o600);
138
+ }
139
+ catch {
140
+ // ignore chmod failures (windows / unusual fs)
141
+ }
142
+ return next;
143
+ }
67
144
  export function clearInstafyCliConfig(keys) {
68
145
  if (!keys || keys.length === 0) {
69
146
  try {
@@ -81,20 +158,70 @@ export function clearInstafyCliConfig(keys) {
81
158
  }
82
159
  writeInstafyCliConfig(next);
83
160
  }
84
- export function resolveConfiguredControllerUrl() {
161
+ export function clearInstafyProfileConfig(profile, keys) {
162
+ const filePath = getInstafyProfileConfigPath(profile);
163
+ if (!keys || keys.length === 0) {
164
+ try {
165
+ fs.rmSync(filePath, { force: true });
166
+ }
167
+ catch {
168
+ // ignore
169
+ }
170
+ return;
171
+ }
172
+ const existing = readInstafyProfileConfig(profile);
173
+ const next = { ...existing };
174
+ for (const key of keys) {
175
+ next[key] = null;
176
+ }
177
+ writeInstafyProfileConfig(profile, next);
178
+ }
179
+ export function resolveActiveProfileName(params) {
180
+ const explicit = normalizeProfileName(params?.profile ?? null);
181
+ if (explicit) {
182
+ return explicit;
183
+ }
184
+ const fromEnv = normalizeProfileName(process.env["INSTAFY_PROFILE"] ?? null);
185
+ if (fromEnv) {
186
+ return fromEnv;
187
+ }
188
+ const cwd = (params?.cwd ?? process.cwd()).trim();
189
+ if (!cwd) {
190
+ return null;
191
+ }
192
+ const manifest = findProjectManifest(cwd).manifest;
193
+ return normalizeProfileName(manifest?.profile ?? null);
194
+ }
195
+ export function resolveConfiguredControllerUrl(params) {
196
+ const profile = resolveActiveProfileName(params);
197
+ if (profile) {
198
+ const configured = readInstafyProfileConfig(profile);
199
+ return normalizeUrl(configured.controllerUrl ?? null);
200
+ }
85
201
  const config = readInstafyCliConfig();
86
202
  return normalizeUrl(config.controllerUrl ?? null);
87
203
  }
88
- export function resolveConfiguredStudioUrl() {
204
+ export function resolveConfiguredStudioUrl(params) {
205
+ const profile = resolveActiveProfileName(params);
206
+ if (profile) {
207
+ const configured = readInstafyProfileConfig(profile);
208
+ return normalizeUrl(configured.studioUrl ?? null);
209
+ }
89
210
  const config = readInstafyCliConfig();
90
211
  return normalizeUrl(config.studioUrl ?? null);
91
212
  }
92
- export function resolveConfiguredAccessToken() {
213
+ export function resolveConfiguredAccessToken(params) {
214
+ const profile = resolveActiveProfileName(params);
215
+ if (profile) {
216
+ const configured = readInstafyProfileConfig(profile);
217
+ return normalizeToken(configured.accessToken ?? null);
218
+ }
93
219
  const config = readInstafyCliConfig();
94
220
  return normalizeToken(config.accessToken ?? null);
95
221
  }
96
222
  export function resolveControllerUrl(params) {
97
- const config = readInstafyCliConfig();
223
+ const profile = resolveActiveProfileName({ profile: params?.profile ?? null, cwd: params?.cwd ?? null });
224
+ const config = profile ? readInstafyProfileConfig(profile) : readInstafyCliConfig();
98
225
  return (normalizeUrl(params?.controllerUrl ?? null) ??
99
226
  normalizeUrl(process.env["INSTAFY_SERVER_URL"] ?? null) ??
100
227
  normalizeUrl(process.env["CONTROLLER_BASE_URL"] ?? null) ??
@@ -102,7 +229,8 @@ export function resolveControllerUrl(params) {
102
229
  "http://127.0.0.1:8788");
103
230
  }
104
231
  export function resolveUserAccessToken(params) {
105
- const config = readInstafyCliConfig();
232
+ const profile = resolveActiveProfileName({ profile: params?.profile ?? null, cwd: params?.cwd ?? null });
233
+ const config = profile ? readInstafyProfileConfig(profile) : readInstafyCliConfig();
106
234
  return (normalizeToken(params?.accessToken ?? null) ??
107
235
  normalizeToken(process.env["INSTAFY_ACCESS_TOKEN"] ?? null) ??
108
236
  normalizeToken(process.env["CONTROLLER_ACCESS_TOKEN"] ?? null) ??
package/dist/git.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
- import { resolveConfiguredAccessToken } from "./config.js";
4
+ import { resolveConfiguredAccessToken, resolveControllerUrl } from "./config.js";
5
5
  function normalizeToken(value) {
6
6
  if (typeof value !== "string") {
7
7
  return null;
@@ -68,10 +68,7 @@ export async function mintGitAccessToken(params) {
68
68
  };
69
69
  }
70
70
  export async function gitToken(options) {
71
- const controllerUrl = options.controllerUrl ??
72
- process.env["INSTAFY_SERVER_URL"] ??
73
- process.env["CONTROLLER_BASE_URL"] ??
74
- "http://127.0.0.1:8788";
71
+ const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
75
72
  const token = resolveControllerAccessToken({
76
73
  controllerAccessToken: options.controllerAccessToken,
77
74
  supabaseAccessToken: options.supabaseAccessToken,
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import { runtimeStart, runtimeStatus, runtimeStop, runtimeToken, findProjectMani
6
6
  import { login, logout } from "./auth.js";
7
7
  import { gitToken } from "./git.js";
8
8
  import { runGitCredentialHelper } from "./git-credential.js";
9
- import { projectInit } from "./project.js";
9
+ import { projectInit, projectProfile } from "./project.js";
10
10
  import { listTunnelSessions, startTunnelDetached, stopTunnelSession, tailTunnelLogs, runTunnelCommand } from "./tunnel.js";
11
11
  import { requestControllerApi } from "./api.js";
12
12
  import { configGet, configList, configPath, configSet, configUnset } from "./config-command.js";
13
+ import { getInstafyProfileConfigPath, listInstafyProfileNames, readInstafyProfileConfig } from "./config.js";
13
14
  export const program = new Command();
14
15
  const require = createRequire(import.meta.url);
15
16
  const pkg = require("../package.json");
@@ -40,6 +41,7 @@ program
40
41
  .description("Log in and save an access token for future CLI commands")
41
42
  .option("--studio-url <url>", "Studio web URL (default: staging or localhost)")
42
43
  .option("--server-url <url>", "Instafy server/controller URL")
44
+ .option("--profile <name>", "Save token under a named profile (multi-account support)")
43
45
  .option("--token <token>", "Provide token directly (skips prompt)")
44
46
  .option("--no-git-setup", "Do not configure git credential helper")
45
47
  .option("--no-store", "Do not save token to ~/.instafy/config.json")
@@ -52,6 +54,7 @@ program
52
54
  token: opts.token,
53
55
  gitSetup: opts.gitSetup,
54
56
  noStore: opts.store === false,
57
+ profile: opts.profile,
55
58
  json: opts.json,
56
59
  });
57
60
  }
@@ -63,10 +66,49 @@ program
63
66
  program
64
67
  .command("logout")
65
68
  .description("Clear the saved CLI access token")
69
+ .option("--profile <name>", "Clear the token for a named profile")
66
70
  .option("--json", "Output JSON")
67
71
  .action(async (opts) => {
68
72
  try {
69
- await logout({ json: opts.json });
73
+ await logout({ json: opts.json, profile: opts.profile });
74
+ }
75
+ catch (error) {
76
+ console.error(kleur.red(String(error)));
77
+ process.exit(1);
78
+ }
79
+ });
80
+ program
81
+ .command("profile:list")
82
+ .description("List saved CLI profiles (~/.instafy/profiles)")
83
+ .option("--json", "Output JSON")
84
+ .action(async (opts) => {
85
+ try {
86
+ const names = listInstafyProfileNames();
87
+ const profiles = names.map((name) => {
88
+ const config = readInstafyProfileConfig(name);
89
+ return {
90
+ name,
91
+ path: getInstafyProfileConfigPath(name),
92
+ controllerUrl: config.controllerUrl ?? null,
93
+ studioUrl: config.studioUrl ?? null,
94
+ accessTokenSet: Boolean(config.accessToken),
95
+ updatedAt: config.updatedAt ?? null,
96
+ };
97
+ });
98
+ if (opts.json) {
99
+ console.log(JSON.stringify({ profiles }, null, 2));
100
+ return;
101
+ }
102
+ if (profiles.length === 0) {
103
+ console.log(kleur.yellow("No profiles found."));
104
+ console.log(`Create one with: ${kleur.cyan("instafy login --profile <name>")}`);
105
+ return;
106
+ }
107
+ console.log(kleur.green("Instafy CLI profiles"));
108
+ for (const profile of profiles) {
109
+ const token = profile.accessTokenSet ? kleur.green("token") : kleur.yellow("no-token");
110
+ console.log(`- ${kleur.cyan(profile.name)} (${token})`);
111
+ }
70
112
  }
71
113
  catch (error) {
72
114
  console.error(kleur.red(String(error)));
@@ -80,6 +122,7 @@ const projectInitCommand = program
80
122
  addServerUrlOptions(projectInitCommand);
81
123
  projectInitCommand
82
124
  .option("--access-token <token>", "Instafy or Supabase access token")
125
+ .option("--profile <name>", "CLI profile to use when running commands in this folder")
83
126
  .option("--project-type <type>", "Project type (customer|sandbox)")
84
127
  .option("--org-id <uuid>", "Optional organization id")
85
128
  .option("--org-name <name>", "Optional organization name")
@@ -92,6 +135,7 @@ projectInitCommand
92
135
  path: opts.path,
93
136
  controllerUrl: opts.serverUrl ?? opts.controllerUrl,
94
137
  accessToken: opts.accessToken,
138
+ profile: opts.profile,
95
139
  projectType: opts.projectType,
96
140
  orgId: opts.orgId,
97
141
  orgName: opts.orgName,
@@ -105,6 +149,27 @@ projectInitCommand
105
149
  process.exit(1);
106
150
  }
107
151
  });
152
+ program
153
+ .command("project:profile")
154
+ .description("Get/set the CLI profile for this folder (.instafy/project.json)")
155
+ .argument("[profile]", "Profile name to set (omit to print current)")
156
+ .option("--unset", "Clear the configured profile for this folder")
157
+ .option("--path <dir>", "Directory to search for the manifest (default: cwd)")
158
+ .option("--json", "Output JSON")
159
+ .action(async (profile, opts) => {
160
+ try {
161
+ projectProfile({
162
+ profile,
163
+ unset: opts.unset,
164
+ path: opts.path,
165
+ json: opts.json,
166
+ });
167
+ }
168
+ catch (error) {
169
+ console.error(kleur.red(String(error)));
170
+ process.exit(1);
171
+ }
172
+ });
108
173
  const configCommand = program.command("config").description("Get/set saved CLI configuration");
109
174
  configCommand
110
175
  .command("path")
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ export function findProjectManifest(startDir) {
4
+ let current = path.resolve(startDir);
5
+ const root = path.parse(current).root;
6
+ while (true) {
7
+ const candidate = path.join(current, ".instafy", "project.json");
8
+ if (existsSync(candidate)) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
11
+ if (parsed?.projectId) {
12
+ return { manifest: parsed, path: candidate };
13
+ }
14
+ }
15
+ catch {
16
+ // ignore malformed manifest
17
+ }
18
+ }
19
+ if (current === root)
20
+ break;
21
+ current = path.dirname(current);
22
+ }
23
+ return { manifest: null, path: null };
24
+ }
package/dist/project.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import kleur from "kleur";
4
- import { resolveControllerUrl, resolveUserAccessToken } from "./config.js";
4
+ import { getInstafyProfileConfigPath, resolveControllerUrl, resolveUserAccessToken } from "./config.js";
5
5
  import { formatAuthRequiredError } from "./errors.js";
6
+ import { findProjectManifest } from "./project-manifest.js";
6
7
  async function fetchOrganizations(controllerUrl, token) {
7
8
  const response = await fetch(`${controllerUrl}/orgs`, {
8
9
  headers: {
@@ -111,8 +112,16 @@ export async function listProjects(options) {
111
112
  }
112
113
  export async function projectInit(options) {
113
114
  const rootDir = path.resolve(options.path ?? process.cwd());
114
- const controllerUrl = resolveControllerUrl({ controllerUrl: options.controllerUrl ?? null });
115
- const token = resolveUserAccessToken({ accessToken: options.accessToken ?? null });
115
+ const controllerUrl = resolveControllerUrl({
116
+ controllerUrl: options.controllerUrl ?? null,
117
+ profile: options.profile ?? null,
118
+ cwd: rootDir,
119
+ });
120
+ const token = resolveUserAccessToken({
121
+ accessToken: options.accessToken ?? null,
122
+ profile: options.profile ?? null,
123
+ cwd: rootDir,
124
+ });
116
125
  if (!token) {
117
126
  throw formatAuthRequiredError({
118
127
  retryCommand: "instafy project:init",
@@ -146,6 +155,7 @@ export async function projectInit(options) {
146
155
  orgId: json.org_id ?? org.orgId ?? null,
147
156
  orgName: json.org_name ?? org.orgName ?? null,
148
157
  controllerUrl,
158
+ profile: options.profile ?? null,
149
159
  createdAt: new Date().toISOString(),
150
160
  }, null, 2), "utf8");
151
161
  }
@@ -171,3 +181,47 @@ export async function projectInit(options) {
171
181
  }
172
182
  return json;
173
183
  }
184
+ export function projectProfile(options) {
185
+ const rootDir = path.resolve(options.path ?? process.cwd());
186
+ const manifestInfo = findProjectManifest(rootDir);
187
+ if (!manifestInfo.path || !manifestInfo.manifest) {
188
+ throw new Error("No project configured. Run `instafy project:init` in this folder first.");
189
+ }
190
+ const shouldUpdate = options.unset === true || typeof options.profile === "string";
191
+ const currentProfile = typeof manifestInfo.manifest.profile === "string" && manifestInfo.manifest.profile.trim()
192
+ ? manifestInfo.manifest.profile.trim()
193
+ : null;
194
+ if (!shouldUpdate) {
195
+ if (options.json) {
196
+ console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: currentProfile }, null, 2));
197
+ }
198
+ else {
199
+ console.log(kleur.green("Project profile"));
200
+ console.log(`Manifest: ${manifestInfo.path}`);
201
+ console.log(`Profile: ${currentProfile ?? kleur.yellow("(none)")}`);
202
+ }
203
+ return { path: manifestInfo.path, profile: currentProfile };
204
+ }
205
+ const nextProfileRaw = options.unset === true ? null : typeof options.profile === "string" ? options.profile.trim() : null;
206
+ const nextProfile = nextProfileRaw && nextProfileRaw.length > 0 ? nextProfileRaw : null;
207
+ if (nextProfile) {
208
+ getInstafyProfileConfigPath(nextProfile);
209
+ }
210
+ const updated = { ...manifestInfo.manifest };
211
+ if (nextProfile) {
212
+ updated.profile = nextProfile;
213
+ }
214
+ else {
215
+ delete updated.profile;
216
+ }
217
+ fs.writeFileSync(manifestInfo.path, JSON.stringify(updated, null, 2), "utf8");
218
+ if (options.json) {
219
+ console.log(JSON.stringify({ ok: true, path: manifestInfo.path, profile: nextProfile }, null, 2));
220
+ }
221
+ else {
222
+ console.log(kleur.green("Updated project profile."));
223
+ console.log(`Manifest: ${manifestInfo.path}`);
224
+ console.log(`Profile: ${nextProfile ?? kleur.yellow("(none)")}`);
225
+ }
226
+ return { path: manifestInfo.path, profile: nextProfile };
227
+ }
package/dist/runtime.js CHANGED
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import os from "node:os";
8
8
  import { ensureRatholeBinary } from "./rathole.js";
9
9
  import { resolveConfiguredAccessToken } from "./config.js";
10
+ import { findProjectManifest } from "./project-manifest.js";
10
11
  const INSTAFY_DIR = path.join(os.homedir(), ".instafy");
11
12
  const STATE_FILE = path.join(INSTAFY_DIR, "cli-runtime-state.json");
12
13
  const LOG_DIR = path.join(INSTAFY_DIR, "cli-runtime-logs");
@@ -142,28 +143,7 @@ function normalizeToken(value) {
142
143
  const trimmed = value.trim();
143
144
  return trimmed.length > 0 ? trimmed : null;
144
145
  }
145
- export function findProjectManifest(startDir) {
146
- let current = path.resolve(startDir);
147
- const root = path.parse(current).root;
148
- while (true) {
149
- const candidate = path.join(current, ".instafy", "project.json");
150
- if (existsSync(candidate)) {
151
- try {
152
- const parsed = JSON.parse(readFileSync(candidate, "utf8"));
153
- if (parsed?.projectId) {
154
- return { manifest: parsed, path: candidate };
155
- }
156
- }
157
- catch {
158
- // ignore malformed manifest
159
- }
160
- }
161
- if (current === root)
162
- break;
163
- current = path.dirname(current);
164
- }
165
- return { manifest: null, path: null };
166
- }
146
+ export { findProjectManifest };
167
147
  function readTokenFromFile(filePath) {
168
148
  const normalized = normalizeToken(filePath);
169
149
  if (!normalized) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instafy/cli",
3
- "version": "0.1.8-staging.357",
3
+ "version": "0.1.8-staging.364",
4
4
  "description": "Run Instafy projects locally, link folders to Studio, and share previews/webhooks via tunnels.",
5
5
  "private": false,
6
6
  "publishConfig": {