insta 0.0.63 → 0.0.64

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.
@@ -169,7 +169,7 @@ export async function buildReport(dirArg, opts, deps) {
169
169
  // NOT "save the generated Dockerfile here": it is not standalone (it COPYs the
170
170
  // .nixpacks/nixpkgs-<hash>.nix support files nixpacks writes beside it, which this
171
171
  // directory does not have). The detected commands above are the reusable part.
172
- nextAction: `to deploy this directory, write a Dockerfile at ${userDockerfilePath} — the detected install/start commands above are the starting point; or connect the repo on GitHub to use the nixpacks lane`,
172
+ nextAction: `to deploy this directory, write a Dockerfile at ${userDockerfilePath} — the detected install/start commands above are the starting point; or connect the GitHub repo to the service (\`insta compute connect-repo <owner/repo>\`) to use the nixpacks lane`,
173
173
  }
174
174
  : {
175
175
  id: 'dockerfile',
@@ -226,7 +226,7 @@ export function renderReport(r, explain) {
226
226
  lines.push(`plan for ${r.dir}:`);
227
227
  // The builder line is the first thing read (and the thing an agent scrapes), so it carries the
228
228
  // lane caveat too — "builder: nixpacks" on its own reads as a promise `insta deploy <dir>` breaks.
229
- const lane = r.plan.builder === 'nixpacks' ? ' — GitHub lane only; `insta deploy <dir>` needs a Dockerfile' : '';
229
+ const lane = r.plan.builder === 'nixpacks' ? ' — GitHub lane only (`insta compute connect-repo`); `insta deploy <dir>` needs a Dockerfile' : '';
230
230
  lines.push(` builder: ${r.plan.builder ?? 'none'}${r.plan.providers.length ? ` (providers: ${r.plan.providers.join(', ')})` : ''}${lane}`);
231
231
  if (r.plan.installCommand)
232
232
  lines.push(` install: ${r.plan.installCommand}`);
@@ -15,6 +15,7 @@ export function deployRequestBody(image, branch, opts) {
15
15
  group: opts.group,
16
16
  port: opts.port ? Number(opts.port) : undefined,
17
17
  websocket: opts.websocket ? true : undefined,
18
+ replaceSource: opts.replaceSource ? true : undefined,
18
19
  };
19
20
  }
20
21
  // A port mismatch is the #1 deploy mistake: the app boots "successfully" but the proxy routes to
@@ -45,7 +46,7 @@ export function noDockerfileMessage(absDir) {
45
46
  'Options:',
46
47
  ` - add a Dockerfile to ${absDir} (\`insta build ${absDir}\` prints the install/start commands nixpacks detected, as a starting point)`,
47
48
  ' - deploy a prebuilt image instead: `insta deploy --image <url>`',
48
- " - connect the app's GitHub repo in the console — that lane builds Dockerfile-less repos with nixpacks server-side",
49
+ ' - connect the GitHub repo to the service (`insta compute connect-repo <owner/repo>`) — that lane builds Dockerfile-less repos with nixpacks server-side',
49
50
  ].join('\n');
50
51
  }
51
52
  // Deploy either a prebuilt image (`--image`) or a source directory (positional `<dir>`, built
@@ -0,0 +1,122 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { info, printJson } from '../util.js';
3
+ import { resolveSoleService, parsePort, q } from './services.js';
4
+ export function parseRepoRef(raw) {
5
+ const s = raw.trim().replace(/^https?:\/\//i, '').replace(/^(www\.)?github\.com\//i, '').replace(/\/+$/, '').replace(/\.git$/i, '');
6
+ const m = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(s);
7
+ if (!m)
8
+ throw new Error(`not a GitHub repository reference: ${raw} (use owner/repo or https://github.com/owner/repo)`);
9
+ return { owner: m[1], repo: m[2] };
10
+ }
11
+ // "", ".", "/", "./" all mean the repo root, which the platform spells null.
12
+ function normalizeRootDir(raw) {
13
+ if (raw === undefined)
14
+ return null;
15
+ const s = raw.trim().replace(/^\.\//, '').replace(/^\/+/, '').replace(/\/+$/, '');
16
+ return s === '' || s === '.' ? null : s;
17
+ }
18
+ export function pickCandidate(candidates, rootDir) {
19
+ if (candidates.length === 0)
20
+ throw new Error('no deployable service detected in this repository (no Dockerfile and nothing nixpacks recognises)');
21
+ if (rootDir !== undefined) {
22
+ const want = normalizeRootDir(rootDir);
23
+ const hit = candidates.find((c) => c.rootDir === want);
24
+ if (!hit)
25
+ throw new Error(`no deployable directory at ${want ?? '(repo root)'} — detected: ${candidates.map((c) => c.rootDir ?? '(repo root)').join(', ')}`);
26
+ return hit;
27
+ }
28
+ if (candidates.length === 1)
29
+ return candidates[0];
30
+ throw new Error([
31
+ `this repository has ${candidates.length} deployable directories; pass --root-dir to choose which one deploys into the service:`,
32
+ ...candidates.map((c) => ` ${(c.rootDir ?? '(repo root)').padEnd(24)} ${c.builder}${c.startCommand ? ` start: ${c.startCommand}` : ''}`),
33
+ ].join('\n'));
34
+ }
35
+ // Build/start come from detection only: the platform's nixpacks lane fails a build whose commands differ from it.
36
+ // autoDeploy rides along only when switched off: a public repo 400s on autoDeploy: true.
37
+ export function sourceBody(src, c, o) {
38
+ const repo = src.source === 'app'
39
+ ? { installationId: src.installationId, repoId: src.repoId, owner: src.owner, repo: src.repo }
40
+ : { public: true, owner: src.owner, repo: src.repo };
41
+ return {
42
+ ...repo,
43
+ rootDir: c.rootDir,
44
+ buildCommand: c.buildCommand,
45
+ startCommand: c.startCommand,
46
+ port: o.port !== undefined ? parsePort(o.port) : c.port,
47
+ ...(o.repoBranch ? { branch: o.repoBranch } : {}),
48
+ ...(o.autoDeploy === false ? { autoDeploy: false } : {}),
49
+ };
50
+ }
51
+ export function repoLine(serviceName, s) {
52
+ if (s.type !== 'github')
53
+ return `compute ${serviceName}: no repository connected${s.image ? ` (runs image ${s.image})` : ''} — connect one with \`insta compute connect-repo <owner/repo> ${serviceName}\``;
54
+ const how = s.public
55
+ ? 'public repo, deploys are manual (pushes do not redeploy)'
56
+ : s.auto_deploy ? `every push to ${s.branch} redeploys it` : 'auto-deploy off (pushes do not redeploy)';
57
+ const where = s.root_dir ? ` (${s.root_dir}/)` : '';
58
+ return `compute ${serviceName}: deploys from ${s.owner}/${s.repo}@${s.branch}${where} — ${how}`;
59
+ }
60
+ export async function findInstalledRepo(api, orgId, ref) {
61
+ if (!orgId)
62
+ throw new Error('this directory is linked without an org — set INSTA_ORG_ID alongside INSTA_PROJECT_ID, or link it with `insta project link`');
63
+ const { installations = [] } = await api.request('GET', `/github/installations?orgId=${encodeURIComponent(orgId)}`);
64
+ if (installations.length === 0) {
65
+ throw new Error('no GitHub App installation for this org — connect GitHub in the console first (Add Service → GitHub Repo → Connect GitHub), or pass --public for a public repository');
66
+ }
67
+ for (const inst of installations) {
68
+ const { repos = [] } = await api.request('GET', `/github/installations/${encodeURIComponent(inst.installation_id)}/repos?orgId=${encodeURIComponent(orgId)}`);
69
+ const hit = repos.find((r) => r.owner.toLowerCase() === ref.owner.toLowerCase() && r.repo.toLowerCase() === ref.repo.toLowerCase());
70
+ if (hit)
71
+ return { installationId: Number(inst.installation_id), repoId: hit.id };
72
+ }
73
+ const accounts = installations.map((i) => i.account_login ?? i.installation_id).join(', ');
74
+ throw new Error(`${ref.owner}/${ref.repo} is not visible to the org's GitHub App installation (installed on: ${accounts}) — grant the App access to it in the console (Configure GitHub app), or pass --public for a public repository`);
75
+ }
76
+ async function targetService(api, projectId, branch, serviceName) {
77
+ const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
78
+ return resolveSoleService(services, 'compute', serviceName);
79
+ }
80
+ export async function computeRepo(serviceName, opts) {
81
+ const api = await ApiClient.load();
82
+ const p = await requireProject();
83
+ const branch = opts.branch ?? p.branch;
84
+ const svc = await targetService(api, p.projectId, branch, serviceName);
85
+ // insta-oss keys compute ids per group, not per branch, so the read carries the branch (the cloud ignores it).
86
+ const { source } = await api.request('GET', `/projects/${p.projectId}/services/${svc.id}/source${q(branch)}`);
87
+ if (opts.json)
88
+ return printJson({ service: { id: svc.id, name: svc.name }, source });
89
+ info(repoLine(svc.name, source));
90
+ }
91
+ export async function computeConnectRepo(rawRef, serviceName, opts) {
92
+ const ref = parseRepoRef(rawRef);
93
+ const api = await ApiClient.load();
94
+ const p = await requireProject();
95
+ const svc = await targetService(api, p.projectId, opts.branch ?? p.branch, serviceName);
96
+ const src = opts.public
97
+ ? { source: 'public', ...ref }
98
+ : { source: 'app', ...(await findInstalledRepo(api, p.orgId, ref)), ...ref };
99
+ // Detection must scan the branch that will be built: the build refuses commands that differ from what it detects there.
100
+ const detected = await api.request('POST', `/projects/${p.projectId}/github/detect`, { ...src, ...(opts.repoBranch ? { ref: opts.repoBranch } : {}) });
101
+ const candidate = pickCandidate(detected.services, opts.rootDir);
102
+ const res = await api.request('PUT', `/projects/${p.projectId}/services/${svc.id}/source`, sourceBody(src, candidate, opts));
103
+ if (opts.json)
104
+ return printJson({ ...res, service: { id: svc.id, name: svc.name } });
105
+ const branch = res.source.branch;
106
+ const where = candidate.rootDir ? `${candidate.rootDir}/, ${candidate.builder}` : candidate.builder;
107
+ const now = res.build.queued ? `building ${branch} now` : `${branch} is already live at this commit`;
108
+ const how = src.source === 'public' ? 'deploys are manual from here: pushes will not redeploy (public repo)'
109
+ : opts.autoDeploy === false ? 'auto-deploy is off: redeploy with `insta compute connect-repo` again or from the console'
110
+ : `every push to ${branch} redeploys it`;
111
+ info(`connected ${ref.owner}/${ref.repo} → compute ${svc.name} (${where}): ${now} — ${how}`);
112
+ }
113
+ export async function computeDisconnectRepo(serviceName, opts) {
114
+ const api = await ApiClient.load();
115
+ const p = await requireProject();
116
+ const svc = await targetService(api, p.projectId, opts.branch ?? p.branch, serviceName);
117
+ const res = await api.request('DELETE', `/projects/${p.projectId}/services/${svc.id}/source`);
118
+ if (opts.json)
119
+ return printJson({ ...res, service: { id: svc.id, name: svc.name } });
120
+ info(`disconnected the repository from compute ${svc.name} — it keeps running its current image; pushes no longer deploy it`);
121
+ }
122
+ //# sourceMappingURL=github.js.map
@@ -1,8 +1,8 @@
1
1
  import { ApiClient } from '../api.js';
2
2
  import { info, printJson } from '../util.js';
3
3
  // insta regions — list the regions a postgres/compute service can be created in.
4
- export async function regionsList(opts = {}) {
5
- const api = await ApiClient.load();
4
+ export async function regionsList(opts = {}, deps) {
5
+ const api = deps?.api ?? await ApiClient.load();
6
6
  const { regions } = await api.request('GET', '/regions');
7
7
  if (opts.json)
8
8
  return printJson(regions);
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import * as secretsCmd from './commands/secrets.js';
22
22
  import { deploy } from './commands/deploy.js';
23
23
  import { build } from './commands/build.js';
24
24
  import * as computeCmd from './commands/compute.js';
25
+ import * as githubCmd from './commands/github.js';
25
26
  import * as dbCmd from './commands/db.js';
26
27
  import * as dbQueryCmd from './commands/db-query.js';
27
28
  import * as storageCmd from './commands/storage.js';
@@ -209,6 +210,7 @@ program.command('build [dir]').description('Verify a source directory would buil
209
210
  program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')
210
211
  .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
211
212
  .option('--websocket', 'run a WebSocket app (larger guest + connection-based concurrency)')
213
+ .option('--replace-source', 'the service deploys from a connected GitHub repo: switch it to this image and remove the repo connection (admin); without it such a deploy is refused')
212
214
  .option('--json', 'print the deploy result as JSON (build progress goes to stderr)')
213
215
  .action(guard((dir, o) => deploy(dir, o)));
214
216
  // `insta compute exec` needs the command verbatim after a literal `--`; split it out of argv here,
@@ -244,6 +246,18 @@ const execCmd = compute.command('exec [service]').description("Run a one-shot co
244
246
  // new option cannot reach the CLI surface while the split still reads it as part of the command.
245
247
  for (const [flags, description] of computeCmd.EXEC_OPTIONS)
246
248
  execCmd.option(flags, description);
249
+ compute.command('repo [service]').description('Show what a compute service deploys from: the image it runs, or the GitHub repository — owner/repo, the branch it builds, root directory, and whether pushes redeploy it')
250
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeRepo(service, o)));
251
+ compute.command('connect-repo <owner/repo> [service]').description("Connect a GitHub repository to an EXISTING compute service: the repo is built (its Dockerfile, or nixpacks when there is none) and deployed into that service, and every later push to the tracked repository branch redeploys it. The repo must be reachable through the org's GitHub App installation — connect GitHub in the console first (Add Service → GitHub Repo) — or be public. Build and start commands come from detection and cannot be set. Connecting again replaces the service's current source")
252
+ .option('--public', 'the repo is public and no GitHub App installation is needed (deploys are manual; pushes cannot redeploy)')
253
+ .option('--root-dir <dir>', 'the directory of the repo to build (a monorepo with several deployable directories lists them and exits 1 without it)')
254
+ .option('--repo-branch <name>', "the repository branch to build (default: the repo's default branch)")
255
+ .option('--no-auto-deploy', 'do not rebuild on pushes; redeploy by connecting again or from the console')
256
+ .option('--port <n>', 'port the app listens on (default: detected)')
257
+ .option('--branch <branch>', 'branch (default: current) — the environment the service is on')
258
+ .option('--json').action(guard((ref, service, o) => githubCmd.computeConnectRepo(ref, service, o)));
259
+ compute.command('disconnect-repo [service]').description('Disconnect the GitHub repository from a compute service. The service keeps running its current image; pushes no longer deploy it, and its build history stays')
260
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeDisconnectRepo(service, o)));
247
261
  compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent /data volume. No flag: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan at the default 10Gi, the free cap; larger is paid and plan-capped; the disk mounts at /data on the next deploy); on a volume-bearing one it grows (paid plans; grow-only — a provisioned disk cannot shrink). --delete DESTROYS the disk and ALL its data immediately (no detach, no undo; billing stops now, and suspend fast-wake + scale-out return). Billing is actual data stored — the size is a cap, not a price")
248
262
  .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
249
263
  .option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)')
@@ -7,8 +7,7 @@
7
7
  import * as clack from '@clack/prompts';
8
8
  import { SERVICE_TYPES, assertServiceName, parsePort } from './commands/services.js';
9
9
  import { CliCancel } from './util.js';
10
- // Same order, labels and default names as the dashboard's Add Service menu. Github Repo is left
11
- // out: the platform has no repo path yet, so a CLI entry could only say "coming soon".
10
+ // The CLI connects repos to existing services only; "GitHub Repo" is not a service kind here.
12
11
  export const SERVICE_KINDS = [
13
12
  { id: 'image', label: 'Docker Image', type: 'compute', hint: 'run an existing container image', needsImage: true },
14
13
  { id: 'postgres', label: 'Postgres', type: 'postgres', hint: 'relational DB, usable as soon as it is added', defaultName: 'main-db' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.63",
3
+ "version": "0.0.64",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [