insta 0.1.20 → 0.1.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.
- package/README.md +1 -1
- package/dist/commands/billing.js +2 -2
- package/dist/commands/compute.js +4 -4
- package/dist/commands/postgres.js +1 -1
- package/dist/commands/template-author.js +292 -0
- package/dist/index.js +36 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ build never reaches a production installer.
|
|
|
237
237
|
| `insta build [dir]` · `deploy [dir]` | Verify a source dir would build; deploy a source directory (built remotely) or `--image <url>`. `insta build logs <id>` reads source-build output: `--source archive` (default) uses the deploy operation ID, `--source github` uses a GitHub build ID; `--follow` watches output, `--json` returns one snapshot |
|
|
238
238
|
| `insta cron` | Branch-scoped HTTP schedules: `list` · `create` · `show` · `edit` · `pause` · `resume` · `delete` · `run` · `runs` · `preview`. Expressions are UTC and so is every time printed; `run` is one EXTRA execution and does not consume the next scheduled tick; literal header values are write-only, so `show` lists their names only — a header backed by `--secret-ref` is resolved from a project secret at send time and `show` names that secret too |
|
|
239
239
|
| `insta run <cmd>` | Run a command with the branch bundle injected, nothing written to disk |
|
|
240
|
-
| `insta template` | `list` · `info` · `deploy` |
|
|
240
|
+
| `insta template` | Browse and deploy: `list` · `info` · `deploy`. Author your org's community templates: `create` (from a project, or `--blank`) · `drafts` · `draft` · `edit --patch <file>` · `regenerate` · `publish` · `unpublish` · `delete`. `publish` and `delete` ask on a terminal and need `--yes` anywhere else, and always for an agent |
|
|
241
241
|
| `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
|
|
242
242
|
| `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
|
|
243
243
|
| `insta config` | `install-mcp` · `regions` · `autoupdate` |
|
package/dist/commands/billing.js
CHANGED
|
@@ -2,11 +2,11 @@ import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
|
2
2
|
import { die, info, openUrl, printJson } from '../util.js';
|
|
3
3
|
import { cycleLine, dimensionLines } from './metrics.js';
|
|
4
4
|
// Resolve the target org: explicit --org, else the linked project's org.
|
|
5
|
-
export async function resolveOrgId(opts) {
|
|
5
|
+
export async function resolveOrgId(opts, project = requireProject) {
|
|
6
6
|
if (opts.org)
|
|
7
7
|
return opts.org;
|
|
8
8
|
// `ProjectConfig.orgId` is typed string but INSTA_PROJECT_ID resolves a project with no org.
|
|
9
|
-
const orgId = (await
|
|
9
|
+
const orgId = (await project()).orgId;
|
|
10
10
|
if (!orgId)
|
|
11
11
|
die('INSTA_PROJECT_ID names no organization — set INSTA_ORG_ID, or pass --org <id>');
|
|
12
12
|
return orgId;
|
package/dist/commands/compute.js
CHANGED
|
@@ -624,7 +624,7 @@ export function removeTarget(count, service, opts) {
|
|
|
624
624
|
if (opts.region !== undefined)
|
|
625
625
|
throw new Error('--region does not apply to --remove');
|
|
626
626
|
if (service !== undefined) {
|
|
627
|
-
throw new Error(count !== undefined && count.trim() !== '' && Number.isFinite(Number(count)) ? '--remove
|
|
627
|
+
throw new Error(count !== undefined && count.trim() !== '' && Number.isFinite(Number(count)) ? '--remove names one instance; pass no count' : `unexpected argument: ${service} (usage: insta compute scale [service] --remove <instance>)`);
|
|
628
628
|
}
|
|
629
629
|
return count;
|
|
630
630
|
}
|
|
@@ -634,7 +634,7 @@ export async function computeScale(count, serviceName, opts) {
|
|
|
634
634
|
if (opts.remove !== undefined)
|
|
635
635
|
return computeRemoveInstance(removeTarget(count, serviceName, { remove: opts.remove, region: opts.region }), opts.remove, opts);
|
|
636
636
|
if (count === undefined)
|
|
637
|
-
throw new Error('a replica
|
|
637
|
+
throw new Error('a replica cap is required (or --remove <instance> to drop one named instance)');
|
|
638
638
|
const machineCount = parseCount(count);
|
|
639
639
|
const api = await ApiClient.load();
|
|
640
640
|
const p = await requireProject();
|
|
@@ -646,7 +646,7 @@ export async function computeScale(count, serviceName, opts) {
|
|
|
646
646
|
return;
|
|
647
647
|
if (opts.json)
|
|
648
648
|
return printJson(res.body.service);
|
|
649
|
-
info(`
|
|
649
|
+
info(`set compute ${svc.name} replica cap to ${machineCount}${opts.region ? ` in ${opts.region}` : ''}`);
|
|
650
650
|
}
|
|
651
651
|
// `insta compute scale [service] --remove <instance>` — DELETE /services/:id/instances/:instance.
|
|
652
652
|
async function computeRemoveInstance(serviceName, instance, opts) {
|
|
@@ -660,7 +660,7 @@ async function computeRemoveInstance(serviceName, instance, opts) {
|
|
|
660
660
|
return;
|
|
661
661
|
if (opts.json)
|
|
662
662
|
return printJson(res.body.service);
|
|
663
|
-
info(`removed ${instance} from compute ${svc.name}; ${res.body.service?.machine_count ?? '?'}
|
|
663
|
+
info(`removed ${instance} from compute ${svc.name}; replica cap is now ${res.body.service?.machine_count ?? '?'}`);
|
|
664
664
|
}
|
|
665
665
|
// ---- limits (the resource ceiling; paid plans) ----
|
|
666
666
|
// Parse a human memory value into MB: "512", "512mb", "1gb", "2g", "1.5gb".
|
|
@@ -390,7 +390,7 @@ export async function dbConnect(service, opts) {
|
|
|
390
390
|
process.stderr.write(`psql → postgres/${r.serviceName}${branch ? ` (branch ${branch})` : ''} — a suspended instance wakes on connect, so the first prompt can take a few seconds\n`);
|
|
391
391
|
relayExitCode(await connectWithPsql(r.url));
|
|
392
392
|
}
|
|
393
|
-
async function confirmOnTerminal(question) {
|
|
393
|
+
export async function confirmOnTerminal(question) {
|
|
394
394
|
const answer = await clack.confirm({ message: question, initialValue: false });
|
|
395
395
|
if (clack.isCancel(answer))
|
|
396
396
|
return false;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// `insta template` authoring commands: the org's community templates, from drafts to the gallery.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { agentMode } from '../agent.js';
|
|
4
|
+
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
5
|
+
import { info, printJson, refuse } from '../util.js';
|
|
6
|
+
import { resolveOrgId } from './billing.js';
|
|
7
|
+
import { confirmOnTerminal } from './postgres.js';
|
|
8
|
+
// ---- pure helpers ----
|
|
9
|
+
/** The console editor of a draft: the API host with its leading `api.` label read as `console.`. */
|
|
10
|
+
export function templateEditorUrl(apiUrl, orgId, code) {
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(apiUrl);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const labels = url.hostname.split('.');
|
|
19
|
+
// Any other host has no console this CLI knows of.
|
|
20
|
+
if (labels.length < 2 || labels[0] !== 'api')
|
|
21
|
+
return null;
|
|
22
|
+
url.hostname = ['console', ...labels.slice(1)].join('.');
|
|
23
|
+
return `${url.origin}/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(code)}`;
|
|
24
|
+
}
|
|
25
|
+
/** The status a person reads. */
|
|
26
|
+
export function templateStatusWord(t) {
|
|
27
|
+
if (t.takenDown)
|
|
28
|
+
return 'taken down';
|
|
29
|
+
if (t.status === 'published' && t.hasUnpublishedChanges)
|
|
30
|
+
return 'published, with unpublished edits';
|
|
31
|
+
return t.status;
|
|
32
|
+
}
|
|
33
|
+
const headLine = (t) => `${t.code}: ${t.name} (${templateStatusWord(t)})`;
|
|
34
|
+
/** One aligned row per template of the org, as `template list` prints the gallery. */
|
|
35
|
+
export function draftListLines(templates) {
|
|
36
|
+
if (!templates.length)
|
|
37
|
+
return ['no templates in this org yet, create one with: insta template create'];
|
|
38
|
+
const head = ['CODE', 'STATUS', 'VERSION', 'NAME'];
|
|
39
|
+
const rows = templates.map((t) => [t.code, templateStatusWord(t), t.publishedVersion ?? '-', t.name]);
|
|
40
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
41
|
+
return [head, ...rows].map((r) => r.map((c, i) => (i === r.length - 1 ? c : c.padEnd(widths[i]))).join(' ').trimEnd());
|
|
42
|
+
}
|
|
43
|
+
/** Each requirement with its mark, and what is still missing for an unmet one. */
|
|
44
|
+
export function requirementLines(requirements) {
|
|
45
|
+
return requirements.map((r) => ` ${r.ok ? '✓' : '✗'} ${r.code}${!r.ok && r.items.length ? `: ${r.items.join(', ')}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
/** What still stands between the draft and a publish. */
|
|
48
|
+
export function readinessLines(requirements) {
|
|
49
|
+
const unmet = requirements.filter((r) => !r.ok);
|
|
50
|
+
return unmet.length ? ['not ready to publish yet:', ...requirementLines(unmet)] : ['ready to publish'];
|
|
51
|
+
}
|
|
52
|
+
/** What a regenerate found changed in the project since the draft was last built. */
|
|
53
|
+
export function regenerateChangeLines(c) {
|
|
54
|
+
const rows = [
|
|
55
|
+
['services added', c.addedServices], ['services removed', c.removedServices],
|
|
56
|
+
['variables added', c.addedVariables], ['variables removed', c.removedVariables],
|
|
57
|
+
];
|
|
58
|
+
const lines = rows.filter(([, names]) => names.length).map(([label, names]) => ` ${label}: ${names.join(', ')}`);
|
|
59
|
+
return lines.length ? lines : [' no service or variable was added to or removed from the project'];
|
|
60
|
+
}
|
|
61
|
+
function serviceText(s) {
|
|
62
|
+
const kind = s.type === 'postgres' && s.pgVersion !== undefined ? `postgres ${s.pgVersion}`
|
|
63
|
+
: s.type === 'storage' ? `storage, ${s.public ? 'public' : 'private'}`
|
|
64
|
+
: s.type;
|
|
65
|
+
const bits = [
|
|
66
|
+
kind,
|
|
67
|
+
s.port != null ? `port ${s.port}` : undefined,
|
|
68
|
+
s.image ? `image ${s.image}` : undefined,
|
|
69
|
+
// A null mount path on a volume is the default /data.
|
|
70
|
+
s.volume ? `volume at ${s.mountPath ?? '/data'}` : undefined,
|
|
71
|
+
s.removed ? 'removed' : undefined,
|
|
72
|
+
];
|
|
73
|
+
return `${s.name} (${bits.filter(Boolean).join(', ')})`;
|
|
74
|
+
}
|
|
75
|
+
function variableText(v) {
|
|
76
|
+
const notes = [
|
|
77
|
+
v.originalName ? `renamed from ${v.originalName}` : undefined,
|
|
78
|
+
v.brokenReference ? `broken reference ${v.brokenReference}` : undefined,
|
|
79
|
+
].filter(Boolean);
|
|
80
|
+
return `${v.kind}${v.value !== undefined ? ` ${v.value}` : ''}${v.description ? `: ${v.description}` : ''}${notes.length ? ` (${notes.join(', ')})` : ''}`;
|
|
81
|
+
}
|
|
82
|
+
/** The whole draft as `draft <code>` prints it. */
|
|
83
|
+
export function draftLines(t, editorUrl) {
|
|
84
|
+
const lines = [headLine(t)];
|
|
85
|
+
const field = (label, value) => { if (value)
|
|
86
|
+
lines.push(` ${label.padEnd(9)} ${value}`); };
|
|
87
|
+
field('tagline', t.tagline);
|
|
88
|
+
field('category', t.category);
|
|
89
|
+
field('readme', t.readme ? `${Buffer.byteLength(t.readme, 'utf8')} bytes` : null);
|
|
90
|
+
field('logo', t.logoUrl);
|
|
91
|
+
field('version', t.publishedVersion);
|
|
92
|
+
field('updated', t.updatedAt);
|
|
93
|
+
lines.push(`services (${t.services.length}):`);
|
|
94
|
+
for (const s of t.services) {
|
|
95
|
+
lines.push(` ${serviceText(s)}`);
|
|
96
|
+
const width = Math.max(...s.variables.map((v) => v.name.length));
|
|
97
|
+
for (const v of s.variables)
|
|
98
|
+
lines.push(` ${v.name.padEnd(width)} ${variableText(v)}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('publish requirements:', ...requirementLines(t.publishRequirements));
|
|
101
|
+
if (editorUrl)
|
|
102
|
+
lines.push(`editor: ${editorUrl}`);
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
/** The console editor's PATCH body, as an agent wrote it to a file or to stdin. */
|
|
106
|
+
export function parsePatch(text, from) {
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
// A UTF-8 BOM belongs to the file, and JSON.parse rejects it.
|
|
110
|
+
value = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
throw new Error(`the patch from ${from} is not valid JSON: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
116
|
+
throw new Error(`the patch from ${from} must be one JSON object, the body the console editor sends, for example {"tagline": "Self-hosted analytics"}`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
export const BLANK_NOT_YET = 'this platform does not create blank templates yet. Create a template from a project instead: insta template create --project <id>';
|
|
121
|
+
async function createBlank(api, orgId, body) {
|
|
122
|
+
try {
|
|
123
|
+
return await api.request('POST', `/orgs/${orgId}/templates`, body);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
// No route yet: Fastify's own 404 for a person, the governance hook's refusal for an agent.
|
|
127
|
+
const missing = e instanceof ApiError
|
|
128
|
+
&& ((e.status === 404 && e.body?.error === 'Not Found') || (e.status === 403 && e.body?.error === 'unclassified_agent_action'));
|
|
129
|
+
if (missing)
|
|
130
|
+
throw new Error(BLANK_NOT_YET);
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// A coded answer is a whole sentence for the person: print it without the HTTP status.
|
|
135
|
+
async function platformSentence(call) {
|
|
136
|
+
try {
|
|
137
|
+
return await call;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
if (e instanceof ApiError && typeof e.body?.code === 'string')
|
|
141
|
+
throw new Error(e.message);
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Decode once at the end: a multi-byte character can straddle two chunks.
|
|
146
|
+
export async function readAllStdin(stream = process.stdin) {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
for await (const chunk of stream)
|
|
149
|
+
chunks.push(chunk);
|
|
150
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
151
|
+
}
|
|
152
|
+
const byAgent = (deps) => deps.agent ?? !!agentMode();
|
|
153
|
+
// An agent on a pty and --json are never a person at the prompt.
|
|
154
|
+
const canAsk = (opts, deps) => !opts.json && !byAgent(deps) && (deps.tty ?? (!!process.stdin.isTTY && !!process.stdout.isTTY));
|
|
155
|
+
const noAnswerReason = (deps) => byAgent(deps) ? 'an agent passes --yes only after the person has said yes.' : 'there is no terminal to confirm on.';
|
|
156
|
+
const orgFlag = (opts) => (opts.org ? ` --org ${opts.org}` : '');
|
|
157
|
+
// ---- commands ----
|
|
158
|
+
export async function templateDrafts(opts = {}, deps = {}) {
|
|
159
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
160
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
161
|
+
const { templates } = await api.request('GET', `/orgs/${orgId}/templates`);
|
|
162
|
+
if (opts.json)
|
|
163
|
+
return printJson(templates);
|
|
164
|
+
for (const line of draftListLines(templates))
|
|
165
|
+
info(line);
|
|
166
|
+
}
|
|
167
|
+
export async function templateDraft(code, opts = {}, deps = {}) {
|
|
168
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
169
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
170
|
+
const { template } = await api.request('GET', `/orgs/${orgId}/templates/${encodeURIComponent(code)}`);
|
|
171
|
+
const editorUrl = templateEditorUrl(api.apiUrl, template.orgId, template.code);
|
|
172
|
+
if (opts.json)
|
|
173
|
+
return printJson({ template, editorUrl });
|
|
174
|
+
for (const line of draftLines(template, editorUrl))
|
|
175
|
+
info(line);
|
|
176
|
+
}
|
|
177
|
+
export async function templateCreate(opts = {}, deps = {}) {
|
|
178
|
+
if (opts.blank && opts.project)
|
|
179
|
+
throw new Error('--blank starts a template with no project, so it does not take --project');
|
|
180
|
+
if (opts.org && !opts.blank)
|
|
181
|
+
throw new Error("--org goes with --blank: a draft generated from a project belongs to that project's org");
|
|
182
|
+
// Presence, not truthiness: an empty --name reaches the platform's own sentence.
|
|
183
|
+
const body = opts.name !== undefined ? { name: opts.name } : undefined;
|
|
184
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
185
|
+
let answer;
|
|
186
|
+
if (opts.blank) {
|
|
187
|
+
answer = await createBlank(api, await resolveOrgId(opts, deps.project), body);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const projectId = opts.project ?? (await (deps.project ?? requireProject)()).projectId;
|
|
191
|
+
answer = await api.request('POST', `/projects/${projectId}/template-drafts`, body);
|
|
192
|
+
}
|
|
193
|
+
const { template } = answer;
|
|
194
|
+
const editorUrl = templateEditorUrl(api.apiUrl, template.orgId, template.code);
|
|
195
|
+
if (opts.json)
|
|
196
|
+
return printJson({ template, editorUrl });
|
|
197
|
+
info(`created ${headLine(template)}`);
|
|
198
|
+
if (editorUrl)
|
|
199
|
+
info(`editor: ${editorUrl}`);
|
|
200
|
+
}
|
|
201
|
+
export async function templateEdit(code, opts, deps = {}) {
|
|
202
|
+
const from = opts.patch === '-' ? 'stdin' : opts.patch;
|
|
203
|
+
let text;
|
|
204
|
+
try {
|
|
205
|
+
text = opts.patch === '-' ? await (deps.readStdin ?? readAllStdin)() : readFileSync(opts.patch, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
throw new Error(`cannot read the patch from ${from}: ${e.message}`);
|
|
209
|
+
}
|
|
210
|
+
const patch = parsePatch(text, from);
|
|
211
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
212
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
213
|
+
const path = `/orgs/${orgId}/templates/${encodeURIComponent(code)}`;
|
|
214
|
+
// Without one, the edit applies to the draft as it is now.
|
|
215
|
+
const expectedUpdatedAt = patch.expectedUpdatedAt ?? (await api.request('GET', path)).template.updatedAt;
|
|
216
|
+
const { template } = await platformSentence(api.request('PATCH', path, { ...patch, expectedUpdatedAt }));
|
|
217
|
+
if (opts.json)
|
|
218
|
+
return printJson({ template, editorUrl: templateEditorUrl(api.apiUrl, template.orgId, template.code) });
|
|
219
|
+
info(`saved ${headLine(template)}`);
|
|
220
|
+
for (const line of readinessLines(template.publishRequirements))
|
|
221
|
+
info(line);
|
|
222
|
+
}
|
|
223
|
+
export async function templateRegenerate(code, opts = {}, deps = {}) {
|
|
224
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
225
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
226
|
+
let answer;
|
|
227
|
+
try {
|
|
228
|
+
answer = await api.request('POST', `/orgs/${orgId}/templates/${encodeURIComponent(code)}/regenerate`);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
// Its 400s are sentences for the person: a blank draft, a deleted source project.
|
|
232
|
+
if (e instanceof ApiError && e.status === 400)
|
|
233
|
+
throw new Error(e.message);
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
const { template, changes } = answer;
|
|
237
|
+
if (opts.json)
|
|
238
|
+
return printJson({ template, changes, editorUrl: templateEditorUrl(api.apiUrl, template.orgId, template.code) });
|
|
239
|
+
info(`regenerated ${headLine(template)}`);
|
|
240
|
+
for (const line of regenerateChangeLines(changes))
|
|
241
|
+
info(line);
|
|
242
|
+
for (const line of readinessLines(template.publishRequirements))
|
|
243
|
+
info(line);
|
|
244
|
+
}
|
|
245
|
+
export async function templatePublish(code, opts = {}, deps = {}) {
|
|
246
|
+
if (!opts.yes && !canAsk(opts, deps)) {
|
|
247
|
+
refuse([
|
|
248
|
+
`refusing to publish ${code} without --yes: ${noAnswerReason(deps)}`,
|
|
249
|
+
`Publishing lists the template in the community gallery at once, with no review. Ask the person first, then run: insta template publish ${code}${orgFlag(opts)} --yes`,
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
253
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
254
|
+
const path = `/orgs/${orgId}/templates/${encodeURIComponent(code)}`;
|
|
255
|
+
const { template: current } = await api.request('GET', path);
|
|
256
|
+
if (!opts.yes) {
|
|
257
|
+
info(`${code} becomes public in the community gallery at once, with no review. Anyone can find it and deploy it.`);
|
|
258
|
+
if (!(await (deps.confirm ?? confirmOnTerminal)(`Publish ${code} now?`)))
|
|
259
|
+
return info('nothing was published');
|
|
260
|
+
}
|
|
261
|
+
const { template } = await platformSentence(api.request('POST', `${path}/publish`, { expectedUpdatedAt: current.updatedAt }));
|
|
262
|
+
if (opts.json)
|
|
263
|
+
return printJson({ template, editorUrl: templateEditorUrl(api.apiUrl, template.orgId, template.code) });
|
|
264
|
+
info(`published ${template.code} version ${template.publishedVersion} to the community gallery`);
|
|
265
|
+
info(`take it out again with: insta template unpublish ${template.code}${orgFlag(opts)}`);
|
|
266
|
+
}
|
|
267
|
+
export async function templateUnpublish(code, opts = {}, deps = {}) {
|
|
268
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
269
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
270
|
+
const { template } = await api.request('POST', `/orgs/${orgId}/templates/${encodeURIComponent(code)}/unpublish`);
|
|
271
|
+
if (opts.json)
|
|
272
|
+
return printJson({ template, editorUrl: templateEditorUrl(api.apiUrl, template.orgId, template.code) });
|
|
273
|
+
info(`unpublished ${template.code}: it is out of the community gallery, and deployed copies keep running`);
|
|
274
|
+
}
|
|
275
|
+
export async function templateDelete(code, opts = {}, deps = {}) {
|
|
276
|
+
if (!opts.yes && !canAsk(opts, deps)) {
|
|
277
|
+
refuse([
|
|
278
|
+
`refusing to delete the draft ${code} without --yes: ${noAnswerReason(deps)}`,
|
|
279
|
+
`Deleting cannot be undone. To go ahead, run: insta template delete ${code}${orgFlag(opts)} --yes`,
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
const api = deps.api ?? (await ApiClient.load());
|
|
283
|
+
const orgId = await resolveOrgId(opts, deps.project);
|
|
284
|
+
if (!opts.yes && !(await (deps.confirm ?? confirmOnTerminal)(`Delete the draft ${code}? This cannot be undone.`))) {
|
|
285
|
+
return info('nothing was deleted');
|
|
286
|
+
}
|
|
287
|
+
await api.request('DELETE', `/orgs/${orgId}/templates/${encodeURIComponent(code)}`);
|
|
288
|
+
if (opts.json)
|
|
289
|
+
return printJson({ ok: true, orgId, code });
|
|
290
|
+
info(`deleted the draft ${code}`);
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=template-author.js.map
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import * as managedDb from './commands/managed-db.js';
|
|
|
33
33
|
import * as storageCmd from './commands/storage.js';
|
|
34
34
|
import { manifest } from './commands/manifest.js';
|
|
35
35
|
import * as template from './commands/template.js';
|
|
36
|
+
import * as templateAuthor from './commands/template-author.js';
|
|
36
37
|
import * as govern from './commands/govern.js';
|
|
37
38
|
import * as observe from './commands/observe.js';
|
|
38
39
|
import * as obs from './commands/metrics.js';
|
|
@@ -374,9 +375,9 @@ compute.command('restart [service]').description("Restart a compute service by r
|
|
|
374
375
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeRestart(service, o)));
|
|
375
376
|
compute.command('status [service]').description("Show a compute service's desired vs. live state")
|
|
376
377
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
|
|
377
|
-
compute.command('scale [count] [service]').description('Set a compute service same-region replica
|
|
378
|
+
compute.command('scale [count] [service]').description('Set a compute service same-region replica cap, 1 to 10 (paid plans only). A scale-to-zero service runs 1 up to the cap on demand; an always-on service runs exactly the cap, so turn always-on on to hold a fixed count. --remove <instance> instead drops that one instance: an always-on service runs one fewer, a scale-to-zero one keeps its cap; instance ids are the `instance` field of `insta compute logs --json`. The last instance cannot be removed')
|
|
378
379
|
.option('--region <region>', 'region to scale in (default: the service region)')
|
|
379
|
-
.option('--remove <instance>', 'drop this instance (inst-<12 hex>)
|
|
380
|
+
.option('--remove <instance>', 'drop this instance (inst-<12 hex>)')
|
|
380
381
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
381
382
|
.action(guard((count, service, o) => computeCmd.computeScale(count, service, o)));
|
|
382
383
|
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
|
|
@@ -535,7 +536,7 @@ program.command('run <cmd> [args...]').description('Run a command with the branc
|
|
|
535
536
|
.passThroughOptions().allowUnknownOption()
|
|
536
537
|
.action(guard((cmd, args, o) => runCmd.run([cmd, ...(args ?? [])], o)));
|
|
537
538
|
// ---- templates (registry, local insta.template.yaml, or a GitHub URL) ----
|
|
538
|
-
const tpl = program.command('template').description(
|
|
539
|
+
const tpl = program.command('template').description("Browse and deploy app templates (registry, a local dir, or a GitHub URL), and author your org's community templates");
|
|
539
540
|
tpl.command('list').description('List templates in the platform registry').option('--json').action(guard((o) => template.templateList(o)));
|
|
540
541
|
tpl.command('info <code>').description('Show a template: version, upstream pin, services, and its required/optional variables')
|
|
541
542
|
.option('--json').action(guard((code, o) => template.templateInfo(code, o)));
|
|
@@ -546,6 +547,38 @@ tpl.command('deploy <code-or-dir-or-url>').description('Deploy a template onto a
|
|
|
546
547
|
.option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
|
|
547
548
|
.option('--json')
|
|
548
549
|
.action(guard((target, o) => template.templateDeploy(target, o)));
|
|
550
|
+
// Authoring: the org's community templates, the same drafts the console editor changes.
|
|
551
|
+
tpl.command('create').description("Create a community template draft from the linked project (or --project), or an empty one with --blank. Prints its code, status and the console editor link")
|
|
552
|
+
.option('--project <id>', 'project to generate the draft from (default: the linked project)')
|
|
553
|
+
.option('--blank', 'start with no project and no services (needs a platform that creates blank templates)')
|
|
554
|
+
.option('--name <name>', 'template name, 1 to 80 characters (default: the project name, or New template with --blank)')
|
|
555
|
+
.option('--org <id>', "org of a --blank draft (default: linked project's org)")
|
|
556
|
+
.option('--json')
|
|
557
|
+
.action(guard((o) => templateAuthor.templateCreate(o)));
|
|
558
|
+
tpl.command('drafts').description("List your org's community templates, drafts and published, with their status")
|
|
559
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
560
|
+
.action(guard((o) => templateAuthor.templateDrafts(o)));
|
|
561
|
+
tpl.command('draft <code>').description('Show one community template draft: its services, every variable with its choice, and the publish requirements')
|
|
562
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
563
|
+
.action(guard((code, o) => templateAuthor.templateDraft(code, o)));
|
|
564
|
+
tpl.command('edit <code>').description("Change a draft with the console editor's PATCH body as JSON. Without expectedUpdatedAt in it, the draft's current one is used")
|
|
565
|
+
.requiredOption('--patch <file>', 'JSON file holding the PATCH body, or - to read it from stdin')
|
|
566
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
567
|
+
.action(guard((code, o) => templateAuthor.templateEdit(code, o)));
|
|
568
|
+
tpl.command('regenerate <code>').description('Rebuild a draft from its project as the project is now, keeping your edits. A blocked item is fixed in the project, then regenerated')
|
|
569
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
570
|
+
.action(guard((code, o) => templateAuthor.templateRegenerate(code, o)));
|
|
571
|
+
tpl.command('publish <code>').description('Publish the draft to the community gallery: public at once, with no review. Asks on a terminal, needs --yes anywhere else, and always for an agent')
|
|
572
|
+
.option('-y, --yes', "publish without asking (get the person's yes first)")
|
|
573
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
574
|
+
.action(guard((code, o) => templateAuthor.templatePublish(code, o)));
|
|
575
|
+
tpl.command('unpublish <code>').description('Take the template out of the community gallery. Deployed copies keep running')
|
|
576
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
577
|
+
.action(guard((code, o) => templateAuthor.templateUnpublish(code, o)));
|
|
578
|
+
tpl.command('delete <code>').description('Delete a draft that was never published. Asks on a terminal, needs --yes anywhere else, and always for an agent')
|
|
579
|
+
.option('-y, --yes', 'delete without asking')
|
|
580
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
581
|
+
.action(guard((code, o) => templateAuthor.templateDelete(code, o)));
|
|
549
582
|
// ---- billing ----
|
|
550
583
|
const bill = program.command('billing').description('Billing: current cycle overview (bare), subscribe to a tier, redeem a credit code, Stripe portal, usage by dimension')
|
|
551
584
|
.option('--org <id>', 'target org (default: linked project\'s org)').option('--json')
|