insta 0.0.21 → 0.0.22

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.
@@ -0,0 +1,12 @@
1
+ import { ApiClient } from '../api.js';
2
+ import { info, printJson } from '../util.js';
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();
6
+ const { regions } = await api.request('GET', '/regions');
7
+ if (opts.json)
8
+ return printJson(regions);
9
+ for (const r of regions)
10
+ info(`${r.slug} ${r.label}`);
11
+ }
12
+ //# sourceMappingURL=regions.js.map
@@ -28,15 +28,61 @@ export async function secrets(opts) {
28
28
  info(` .gitignore += ${out} (credentials must never be committed)`);
29
29
  info(' tip: `insta run -- <cmd>` injects these per-run with nothing written to disk');
30
30
  }
31
+ // Render one branch's service-grouped secrets, then its unbound (branch-level) secrets.
32
+ function renderBranch(b) {
33
+ for (const s of b.services)
34
+ if (s.secrets.length) {
35
+ info(` ${s.type}/${s.name}`);
36
+ for (const n of s.secrets)
37
+ info(` ${n}`);
38
+ }
39
+ if (b.unbound.length) {
40
+ info(' (branch-level)');
41
+ for (const n of b.unbound)
42
+ info(` ${n}`);
43
+ }
44
+ }
45
+ // Show the full binding tree: project-wide, then every branch grouped by service.
46
+ export async function secretsTree(opts) {
47
+ const api = await ApiClient.load();
48
+ const p = await requireProject();
49
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
50
+ if (handleApproval(res))
51
+ return;
52
+ const tree = res.body;
53
+ if (opts.json)
54
+ return printJson(tree);
55
+ if (tree.projectWide.length) {
56
+ info('(project-wide)');
57
+ for (const n of tree.projectWide)
58
+ info(` ${n}`);
59
+ }
60
+ for (const b of tree.branches) {
61
+ info(`${b.name}${b.isDefault ? ' *' : ''}`);
62
+ renderBranch(b);
63
+ }
64
+ }
65
+ // List secret names for the current (or given) branch, grouped by service.
31
66
  export async function secretsList(opts) {
32
67
  const api = await ApiClient.load();
33
68
  const p = await requireProject();
34
69
  const branch = opts.branch ?? p.branch;
35
- const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`);
70
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`);
36
71
  if (handleApproval(res))
37
72
  return;
38
- for (const name of Object.keys(res.body.secrets))
39
- info(name);
73
+ const tree = res.body;
74
+ const b = tree.branches.find((x) => x.name === branch);
75
+ if (opts.json)
76
+ return printJson({ projectWide: tree.projectWide, branch: b });
77
+ if (tree.projectWide.length) {
78
+ info('(project-wide)');
79
+ for (const n of tree.projectWide)
80
+ info(` ${n}`);
81
+ }
82
+ if (b) {
83
+ info(`${b.name}`);
84
+ renderBranch(b);
85
+ }
40
86
  }
41
87
  async function readStdin() {
42
88
  let data = '';
@@ -44,19 +90,21 @@ async function readStdin() {
44
90
  data += chunk;
45
91
  return data.trim();
46
92
  }
47
- // Set a user secret. Project-wide by default; --branch scopes it to one branch. Value comes from
48
- // the argument, or stdin when omitted (keeps secret values out of shell history).
93
+ // Set a user secret. Project-wide by default; --branch scopes it to one branch. --service binds
94
+ // it to a branch service instead, which implies the current branch (binding requires one). Value
95
+ // comes from the argument, or stdin when omitted (keeps secret values out of shell history).
49
96
  export async function secretsSet(name, value, opts) {
50
97
  const api = await ApiClient.load();
51
98
  const p = await requireProject();
52
99
  const v = value ?? (await readStdin());
53
100
  if (!v)
54
101
  die('value is required (pass as an argument or on stdin)');
55
- const payload = opts.branch ? { value: v, branch: opts.branch } : { value: v };
102
+ const branch = opts.service ? (opts.branch ?? p.branch) : opts.branch;
103
+ const payload = { value: v, ...(branch ? { branch } : {}), ...(opts.service ? { service: opts.service } : {}) };
56
104
  const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload);
57
105
  if (handleApproval(res))
58
106
  return;
59
- info(`set ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`);
107
+ info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`);
60
108
  }
61
109
  export async function secretsUnset(name, opts) {
62
110
  const api = await ApiClient.load();
@@ -2,6 +2,7 @@
2
2
  import { ApiClient, requireProject } from '../api.js';
3
3
  import { info, printJson, handleApproval, renderNextActions } from '../util.js';
4
4
  export const SERVICE_TYPES = ['postgres', 'storage', 'compute'];
5
+ const SERVICE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,38}$/;
5
6
  export function q(branch) {
6
7
  return branch ? `?branch=${encodeURIComponent(branch)}` : '';
7
8
  }
@@ -11,6 +12,10 @@ export function assertType(type, allowed = SERVICE_TYPES) {
11
12
  if (!allowed.includes(type))
12
13
  throw new Error(`type must be ${allowed.join('|')}`);
13
14
  }
15
+ export function assertServiceName(name) {
16
+ if (!SERVICE_NAME_RE.test(name))
17
+ throw new Error('service name must be lower-kebab (a-z, 0-9, -)');
18
+ }
14
19
  // Parse a positive-integer machine count.
15
20
  export function parseCount(raw) {
16
21
  const n = Number(raw);
@@ -40,22 +45,46 @@ export function resolveComputeServiceId(services, name) {
40
45
  throw new Error(`multiple compute services — specify one: ${compute.map((s) => s.name).join(', ')}`);
41
46
  return compute[0].id;
42
47
  }
43
- // ---- commands ----
48
+ // Map service-add options to the platform POST body. Pure, so it's unit-tested without a network
49
+ // mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which
50
+ // type) stays in servicesAdd, ahead of any network/config access.
51
+ export function servicesAddRequestBody(type, name, branch, opts) {
52
+ return {
53
+ type, name, ...(branch ? { branch } : {}), public: !!opts.public,
54
+ ...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: Number(opts.port) } : {}),
55
+ ...(opts.region ? { region: opts.region } : {}),
56
+ };
57
+ }
44
58
  export async function servicesAdd(type, name, opts = {}) {
45
59
  assertType(type);
46
60
  if (opts.public && type !== 'storage')
47
61
  throw new Error('--public is only valid for storage services');
62
+ if (opts.region && type === 'storage')
63
+ throw new Error('--region is not valid for storage services');
64
+ if (opts.image && type !== 'compute')
65
+ throw new Error('--image is only valid for compute services');
66
+ if (opts.port && type !== 'compute')
67
+ throw new Error('--port is only valid for compute services');
48
68
  const api = await ApiClient.load();
49
69
  const p = await requireProject();
50
70
  const branch = opts.branch ?? p.branch;
51
- const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, ...(branch ? { branch } : {}), public: !!opts.public });
71
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts));
52
72
  if (handleApproval(res))
53
73
  return;
54
74
  const svc = res.body.service;
55
75
  const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
56
- info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.domain ? ` — ${svc.domain}` : ''}`);
76
+ const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
77
+ info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${svc.domain ? ` — ${svc.domain}` : ''}`);
57
78
  renderNextActions(res.body.nextActions);
58
79
  }
80
+ // Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
81
+ // billingLines in billing.ts). Compute rows show the running image when the platform reports one.
82
+ export function serviceListLine(s) {
83
+ const extra = s.type === 'compute'
84
+ ? ` x${s.machine_count}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
85
+ : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
86
+ return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
87
+ }
59
88
  export async function servicesList(opts) {
60
89
  const api = await ApiClient.load();
61
90
  const p = await requireProject();
@@ -65,10 +94,8 @@ export async function servicesList(opts) {
65
94
  return printJson(services);
66
95
  if (!services.length)
67
96
  return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`);
68
- for (const s of services) {
69
- const extra = s.type === 'compute' ? ` x${s.machine_count}` : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
70
- info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`);
71
- }
97
+ for (const s of services)
98
+ info(serviceListLine(s));
72
99
  }
73
100
  export async function servicesRemove(type, name, opts = {}) {
74
101
  assertType(type);
@@ -82,6 +109,21 @@ export async function servicesRemove(type, name, opts = {}) {
82
109
  return;
83
110
  info(`removed ${type} service ${name} from ${branch ?? 'default'}`);
84
111
  }
112
+ export async function servicesRename(type, name, newName, opts = {}) {
113
+ assertType(type);
114
+ assertServiceName(newName);
115
+ const api = await ApiClient.load();
116
+ const p = await requireProject();
117
+ const branch = opts.branch ?? p.branch;
118
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
119
+ const id = resolveServiceId(services, type, name);
120
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/rename`, { name: newName });
121
+ if (handleApproval(res))
122
+ return;
123
+ if (opts.json)
124
+ return printJson(res.body.service);
125
+ info(`renamed ${type} service ${name} to ${newName}`);
126
+ }
85
127
  // Validate a bucket access-mode argument.
86
128
  export function parseAccess(raw) {
87
129
  if (raw === 'public')
@@ -134,4 +176,22 @@ export async function servicesUpgrade(type, name, spec, _opts) {
134
176
  return printJson(res.body.service);
135
177
  info(`upgraded ${type} ${name} to ${spec}`);
136
178
  }
179
+ // insta services secrets <type> <name> — the secret names bound to a service.
180
+ export async function servicesSecrets(type, name, opts = {}) {
181
+ assertType(type);
182
+ const api = await ApiClient.load();
183
+ const p = await requireProject();
184
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`);
185
+ const id = resolveServiceId(services, type, name);
186
+ const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
187
+ if (handleApproval(res))
188
+ return;
189
+ const { secrets } = res.body;
190
+ if (opts.json)
191
+ return printJson(secrets);
192
+ if (!secrets.length)
193
+ return info(`(no secrets bound to ${type}/${name})`);
194
+ for (const n of secrets)
195
+ info(n);
196
+ }
137
197
  //# sourceMappingURL=services.js.map
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import * as org from './commands/org.js';
11
11
  import * as project from './commands/project.js';
12
12
  import * as branch from './commands/branch.js';
13
13
  import * as services from './commands/services.js';
14
+ import * as regions from './commands/regions.js';
14
15
  import * as secretsCmd from './commands/secrets.js';
15
16
  import { deploy } from './commands/deploy.js';
16
17
  import * as computeCmd from './commands/compute.js';
@@ -97,28 +98,39 @@ br.command('merge <source>').description('Merge a branch service set into anothe
97
98
  const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)');
98
99
  svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
99
100
  .option('--branch <branch>', 'target branch (default: current)')
101
+ .option('--region <region>', 'region for postgres/compute, e.g. us-east (see `insta regions`)')
100
102
  .option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
103
+ .option('--image <url>', 'compute only: run this container image at creation')
104
+ .option('--port <n>', 'compute only: port the image listens on (default 8080)')
101
105
  .action(guard((type, name, o) => services.servicesAdd(type, name, o)));
102
106
  svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
103
107
  .action(guard((o) => services.servicesList(o)));
104
108
  svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
105
109
  .option('--branch <branch>', 'branch (default: current)')
106
110
  .action(guard((type, name, o) => services.servicesRemove(type, name, o)));
111
+ svc.command('rename <type> <name> <new-name>').description('Rename a service and re-key its managed secret names')
112
+ .option('--json').option('--branch <branch>', 'branch (default: current)')
113
+ .action(guard((type, name, newName, o) => services.servicesRename(type, name, newName, o)));
107
114
  svc.command('set-access <type> <name> <access>').description('Set a storage service bucket access mode (access: public|private)')
108
115
  .option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)));
109
116
  svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
110
117
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
111
118
  svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
112
119
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
120
+ svc.command('secrets <type> <name>').description("List a service's secret names")
121
+ .option('--branch <b>').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o)));
113
122
  // ---- secrets (seam) ----
114
123
  const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
115
124
  .option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
116
125
  .action(guard((o) => secretsCmd.secrets(o)));
117
- sec.command('list').description('List secret names only').option('--branch <branch>').action(guard((o) => secretsCmd.secretsList(o)));
126
+ sec.command('list').description('List secret names, grouped by service').option('--branch <branch>').option('--json').action(guard((o) => secretsCmd.secretsList(o)));
118
127
  sec.command('set <name> [value]').description('Set a user secret (project-wide; value from stdin if omitted)')
119
- .option('--branch <branch>', 'scope to one branch').action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
128
+ .option('--branch <branch>', 'scope to one branch').option('--service <type/name>', 'bind to a branch service (implies current branch)')
129
+ .action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)));
120
130
  sec.command('unset <name>').description('Remove a user secret')
121
131
  .option('--branch <branch>', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o)));
132
+ sec.command('tree').description('Show secrets as project → branch → service → secrets').option('--json')
133
+ .action(guard((o) => secretsCmd.secretsTree(o)));
122
134
  // ---- deploy ----
123
135
  program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')
124
136
  .option('--image <url>', 'prebuilt container image to deploy (instead of a source dir)').option('--branch <b>').option('--group <g>').option('--port <p>')
@@ -142,6 +154,8 @@ compute.command('status [service]').description("Show a compute service's desire
142
154
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
143
155
  // ---- manifest ----
144
156
  program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
157
+ // ---- regions ----
158
+ program.command('regions').description('List regions available for postgres/compute services').option('--json').action(guard((o) => regions.regionsList(o)));
145
159
  // ---- observability ----
146
160
  program.command('metrics <target> [group]').description('Service metrics (target: db|compute)')
147
161
  .option('--branch <b>').option('--from <unix>').option('--to <unix>').option('--step <s>').option('--json')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI \u2014 a thin client of the platform control-plane API.",
6
6
  "keywords": [