insta 0.1.19 → 0.1.21
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 +33 -4
- package/dist/commands/postgres.js +1 -1
- package/dist/commands/template-author.js +292 -0
- package/dist/index.js +34 -1
- 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
|
@@ -68,10 +68,26 @@ export function domainGuidanceLines(r, ctx = {}) {
|
|
|
68
68
|
// where the hostname resolves — we cannot confirm routing, and must not call it serving. A Fly
|
|
69
69
|
// answer carries no `ssl` and has no per-hostname origin concept, so its own verdict stands.
|
|
70
70
|
const reportsOrigin = (r) => r.ssl !== undefined;
|
|
71
|
+
// A deployment with NO edge custody for custom domains (a pre-Cloudflare plane). It reports
|
|
72
|
+
// `origin: ""` exactly like a region whose operator forgot cf-custom-origin, so the pair alone reads
|
|
73
|
+
// it backwards and names a setting this deployment does not have. The plane's own verdict
|
|
74
|
+
// (`originStatus: 'unmanaged'`) decides when relayed; without it, `ssl: 'external'` is the plane's
|
|
75
|
+
// marker for the same deployment class (it manages no edge certificate here) — the same fallback the
|
|
76
|
+
// console uses. Only an unconfirmed origin is reinterpreted: a reported, confirmed origin stands.
|
|
77
|
+
const edgeUnmanaged = (r) => r.originStatus === 'unmanaged' ||
|
|
78
|
+
(r.originStatus === undefined && r.ssl === 'external' && (r.origin === undefined || r.origin === ''));
|
|
71
79
|
// Where the hostname actually resolves — the region-specific origin the plane requested vs what
|
|
72
80
|
// Cloudflare holds. Each shape carries its action; absent fields are reported as absent.
|
|
73
81
|
export function domainResolveLine(r) {
|
|
74
82
|
const region = r.region ?? 'this region';
|
|
83
|
+
if (edgeUnmanaged(r)) {
|
|
84
|
+
// Not ready (routing cannot be confirmed from here), but no operator knob to name either:
|
|
85
|
+
// setting cf-custom-origin does nothing on a deployment with no edge custody.
|
|
86
|
+
return {
|
|
87
|
+
line: ` ${pad('resolves to', 12)}UNMANAGED — ${region} does not manage edge certificates for custom domains, so where ${r.hostname} lands cannot be confirmed from here; if it does not serve, contact support`,
|
|
88
|
+
ready: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
75
91
|
if (r.origin === undefined) {
|
|
76
92
|
if (reportsOrigin(r)) {
|
|
77
93
|
return {
|
|
@@ -194,6 +210,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
194
210
|
}
|
|
195
211
|
}
|
|
196
212
|
const ssl = r.ssl ?? (r.configured ? 'active' : undefined);
|
|
213
|
+
const reasonOnCert = !!r.errorReason && r.status !== 'error' && ssl !== undefined && ssl !== 'active' && ssl !== 'external';
|
|
197
214
|
if (ssl === 'active')
|
|
198
215
|
stage('certificate', 'active', '(edge TLS issued)');
|
|
199
216
|
else if (ssl === 'external')
|
|
@@ -203,7 +220,11 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
203
220
|
blockers.push('certificate');
|
|
204
221
|
}
|
|
205
222
|
else {
|
|
206
|
-
|
|
223
|
+
// A pending certificate with a reason from the plane: the reason IS why it has not issued (a CAA
|
|
224
|
+
// block, the hostname not pointed at the edge), so it replaces the generic text here and is not
|
|
225
|
+
// repeated below. An `error` row keeps its reason on its own error line.
|
|
226
|
+
const detail = reasonOnCert ? `(${ssl}) ${r.errorReason}` : `(${ssl} — issues once ownership is verified)`;
|
|
227
|
+
stage('certificate', 'pending', detail);
|
|
207
228
|
blockers.push('certificate');
|
|
208
229
|
}
|
|
209
230
|
const resolve = domainResolveLine(r);
|
|
@@ -214,9 +235,17 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
214
235
|
stage('error', r.status, r.errorReason || '(the plane reported an error state with no reason)');
|
|
215
236
|
blockers.push('the plane reports an error state');
|
|
216
237
|
}
|
|
217
|
-
else if (r.errorReason) {
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
else if (r.errorReason && !reasonOnCert) {
|
|
239
|
+
// On an ACTIVE row the plane keeps serving and REPORTS what the edge says has regressed (it
|
|
240
|
+
// never demotes a serving row on an edge read). That is a warning beside a serving hostname,
|
|
241
|
+
// not a blocker: turning it into one would print "not yet" for a domain taking traffic.
|
|
242
|
+
// Every other state with a reason is still being set up, and the reason is what blocks it.
|
|
243
|
+
if (r.status === 'active')
|
|
244
|
+
stage('warning', r.status, r.errorReason);
|
|
245
|
+
else {
|
|
246
|
+
stage('error', r.status, r.errorReason);
|
|
247
|
+
blockers.push(r.errorReason);
|
|
248
|
+
}
|
|
220
249
|
}
|
|
221
250
|
// An unconfirmed routing target is one blocker among the others, not a headline that hides them:
|
|
222
251
|
// a user fixing their DNS needs the whole outstanding list, not whichever item sorted first.
|
|
@@ -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';
|
|
@@ -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')
|