Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ build never reaches a production installer.
| `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 |
| `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 |
| `insta run <cmd>` | Run a command with the branch bundle injected, nothing written to disk |
| `insta template` | `list` · `info` · `deploy` |
| `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 |
| `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
| `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
| `insta config` | `install-mcp` · `regions` · `autoupdate` |
Expand Down
5 changes: 3 additions & 2 deletions src/commands/billing.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ApiClient, ApiError, requireProject } from '../api.js'
import type { ProjectConfig } from '../config.js'
import { die, info, openUrl, printJson } from '../util.js'
import { cycleLine, dimensionLines } from './metrics.js'

type OrgOpt = { org?: string }

// Resolve the target org: explicit --org, else the linked project's org.
export async function resolveOrgId(opts: OrgOpt): Promise<string> {
export async function resolveOrgId(opts: OrgOpt, project: () => Promise<ProjectConfig> = requireProject): Promise<string> {
if (opts.org) return opts.org
// `ProjectConfig.orgId` is typed string but INSTA_PROJECT_ID resolves a project with no org.
const orgId = (await requireProject()).orgId
const orgId = (await project()).orgId
if (!orgId) die('INSTA_PROJECT_ID names no organization — set INSTA_ORG_ID, or pass --org <id>')
return orgId
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ export type AccessDeps = {
tty?: boolean
}

async function confirmOnTerminal(question: string): Promise<boolean> {
export async function confirmOnTerminal(question: string): Promise<boolean> {
const answer = await clack.confirm({ message: question, initialValue: false })
if (clack.isCancel(answer)) return false
return answer === true
Expand Down
324 changes: 324 additions & 0 deletions src/commands/template-author.ts

Large diffs are not rendered by default.

35 changes: 34 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as managedDb from './commands/managed-db.js'
import * as storageCmd from './commands/storage.js'
import { manifest } from './commands/manifest.js'
import * as template from './commands/template.js'
import * as templateAuthor from './commands/template-author.js'
import * as govern from './commands/govern.js'
import * as observe from './commands/observe.js'
import * as obs from './commands/metrics.js'
Expand Down Expand Up @@ -561,7 +562,7 @@ program.command('run <cmd> [args...]').description('Run a command with the branc
.action(guard((cmd, args, o) => runCmd.run([cmd, ...(args ?? [])], o)))

// ---- templates (registry, local insta.template.yaml, or a GitHub URL) ----
const tpl = program.command('template').description('Browse and deploy app templates (registry, a local dir, or a GitHub URL)')
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")
tpl.command('list').description('List templates in the platform registry').option('--json').action(guard((o) => template.templateList(o)))
tpl.command('info <code>').description('Show a template: version, upstream pin, services, and its required/optional variables')
.option('--json').action(guard((code, o) => template.templateInfo(code, o)))
Expand All @@ -572,6 +573,38 @@ tpl.command('deploy <code-or-dir-or-url>').description('Deploy a template onto a
.option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
.option('--json')
.action(guard((target, o) => template.templateDeploy(target, o)))
// Authoring: the org's community templates, the same drafts the console editor changes.
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")
.option('--project <id>', 'project to generate the draft from (default: the linked project)')
.option('--blank', 'start with no project and no services (needs a platform that creates blank templates)')
.option('--name <name>', 'template name, 1 to 80 characters (default: the project name, or New template with --blank)')
.option('--org <id>', "org of a --blank draft (default: linked project's org)")
.option('--json')
.action(guard((o) => templateAuthor.templateCreate(o)))
tpl.command('drafts').description("List your org's community templates, drafts and published, with their status")
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((o) => templateAuthor.templateDrafts(o)))
tpl.command('draft <code>').description('Show one community template draft: its services, every variable with its choice, and the publish requirements')
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templateDraft(code, o)))
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")
.requiredOption('--patch <file>', 'JSON file holding the PATCH body, or - to read it from stdin')
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templateEdit(code, o)))
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')
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templateRegenerate(code, o)))
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')
.option('-y, --yes', "publish without asking (get the person's yes first)")
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templatePublish(code, o)))
tpl.command('unpublish <code>').description('Take the template out of the community gallery. Deployed copies keep running')
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templateUnpublish(code, o)))
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')
.option('-y, --yes', 'delete without asking')
.option('--org <id>', "target org (default: linked project's org)").option('--json')
.action(guard((code, o) => templateAuthor.templateDelete(code, o)))

// ---- billing ----
const bill = program.command('billing').description('Billing: current cycle overview (bare), subscribe to a tier, redeem a credit code, Stripe portal, usage by dimension')
Expand Down
14 changes: 14 additions & 0 deletions test/help-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ describe('group shapes', () => {
expect(r.stdout).toContain('--follow')
expect(r.stdout).toContain('--json')
}, 30_000)
// Authoring follows browsing and deploying: the console editor's drafts, one verb each.
it('template keeps list, info and deploy, then the authoring verbs', () => {
const help = run(['template', '--help']).stdout
expect(commandNames(help)).toEqual(['list', 'info', 'deploy', 'create', 'drafts', 'draft', 'edit', 'regenerate', 'publish', 'unpublish', 'delete'])
expect(help.replace(/\s+/g, ' ')).toContain("and author your org's community templates")
const create = run(['template', 'create', '--help']).stdout
for (const flag of ['--project <id>', '--blank', '--name <name>', '--org <id>', '--json']) expect(create, flag).toContain(flag)
expect(run(['template', 'edit', '--help']).stdout).toContain('Usage: insta template edit [options] <code>')
const noPatch = run(['template', 'edit', 'my-app'])
expect(noPatch.status).not.toBe(0)
expect(noPatch.stderr).toContain("required option '--patch <file>' not specified")
for (const verb of ['publish', 'delete']) expect(run(['template', verb, '--help']).stdout, verb).toContain('-y, --yes')
expect(run(['template', 'unpublish', '--help']).stdout).not.toContain('--yes')
}, 30_000)
it('agent, config and billing carry the moved verbs', () => {
expect(commandNames(run(['agent', '--help']).stdout)).toEqual(['setup', 'manifest', 'policy', 'approvals', 'observe', 'events'])
expect(commandNames(run(['config', '--help']).stdout)).toEqual(['install-mcp', 'regions', 'autoupdate'])
Expand Down
Loading
Loading