From f1bd54a9b6f19f3db6142e8d2b8c958003944222 Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Thu, 8 Oct 2026 14:58:32 -0700 Subject: [PATCH] feat(template): drafts --json prints summaries, and its help says so insta-platform's GET /orgs/{orgId}/templates serves one CommunityTemplateSummary per template since feat/template-list-summary, with no services, variables or publish requirements. template drafts --json prints that array as served, so its --json help says so and names template draft --json for one draft in full. TemplateDraftSummary types the list and draftListLines. The table reads only summary fields and is unchanged. --- src/commands/template-author.ts | 6 +++-- src/index.ts | 3 ++- test/template-author.test.ts | 41 ++++++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/commands/template-author.ts b/src/commands/template-author.ts index b887da5..e295a9e 100644 --- a/src/commands/template-author.ts +++ b/src/commands/template-author.ts @@ -24,6 +24,8 @@ export type TemplateDraft = { readme: string | null; logoUrl: string | null; services: TemplateDraftService[] publishRequirements: TemplateRequirement[]; updatedAt: string } +/** A row of the org's list (the platform's CommunityTemplateSummary), as the table reads it. */ +export type TemplateDraftSummary = Pick type DraftAnswer = { template: TemplateDraft } /** What regenerate found added or removed in the project since the last build. */ export type TemplateChanges = { addedServices: string[]; removedServices: string[]; addedVariables: string[]; removedVariables: string[] } @@ -64,7 +66,7 @@ export function templateStatusWord(t: Pick `${t.code}: ${t.name} (${templateStatusWord(t)})` /** One aligned row per template of the org, as `template list` prints the gallery. */ -export function draftListLines(templates: TemplateDraft[]): string[] { +export function draftListLines(templates: TemplateDraftSummary[]): string[] { if (!templates.length) return ['no templates in this org yet, create one with: insta template create'] const head = ['CODE', 'STATUS', 'VERSION', 'NAME'] const rows = templates.map((t) => [t.code, templateStatusWord(t), t.publishedVersion ?? '-', t.name]) @@ -201,7 +203,7 @@ const orgFlag = (opts: { org?: string }): string => (opts.org ? ` --org ${opts.o export async function templateDrafts(opts: OrgOpts = {}, deps: TemplateAuthorDeps = {}): Promise { const api = deps.api ?? (await ApiClient.load()) const orgId = await resolveOrgId(opts, deps.project) - const { templates } = await api.request<{ templates: TemplateDraft[] }>('GET', `/orgs/${orgId}/templates`) + const { templates } = await api.request<{ templates: TemplateDraftSummary[] }>('GET', `/orgs/${orgId}/templates`) if (opts.json) return printJson(templates) for (const line of draftListLines(templates)) info(line) } diff --git a/src/index.ts b/src/index.ts index bb503e0..b075dc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -582,7 +582,8 @@ tpl.command('create').description("Create a community template draft from the li .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 ', "target org (default: linked project's org)").option('--json') + .option('--org ', "target org (default: linked project's org)") + .option('--json', 'print one summary per template as the platform serves it, with no services, variables or publish requirements. insta template draft --json prints one draft in full') .action(guard((o) => templateAuthor.templateDrafts(o))) tpl.command('draft ').description('Show one community template draft: its services, every variable with its choice, and the publish requirements') .option('--org ', "target org (default: linked project's org)").option('--json') diff --git a/test/template-author.test.ts b/test/template-author.test.ts index 1a49305..1132e67 100644 --- a/test/template-author.test.ts +++ b/test/template-author.test.ts @@ -1,6 +1,8 @@ // Template authoring (spec 2026-10-06 ยง3): a real ApiClient over a fake fetch. import { afterAll, afterEach, describe, expect, it, vi } from 'vitest' +import { spawnSync } from 'node:child_process' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { tmpdir } from 'node:os' import { Readable } from 'node:stream' @@ -10,7 +12,7 @@ import type { ProjectConfig } from '../src/config.js' import { templateEditorUrl, templateStatusWord, draftListLines, requirementLines, readinessLines, regenerateChangeLines, draftLines, parsePatch, readAllStdin, templateDrafts, templateDraft, templateCreate, templateEdit, templateRegenerate, templatePublish, templateUnpublish, - templateDelete, BLANK_NOT_YET, type TemplateDraft, + templateDelete, BLANK_NOT_YET, type TemplateDraft, type TemplateDraftSummary, } from '../src/commands/template-author.js' const API = 'https://api.instacloud.com' @@ -50,6 +52,14 @@ const view = (over: Partial & Record = {}): Temp ...over, } as TemplateDraft) +// The platform's CommunityTemplateSummary, one row of the org list, every key present. +const summary = (over: Partial & Record = {}): TemplateDraftSummary => ({ + id: '11111111-0000-4000-8000-000000000001', code: 'my-app', orgId: ORG, sourceProjectId: PROJECT, blank: false, + status: 'draft', publishedVersion: null, hasUnpublishedChanges: true, takenDown: false, + name: 'My App', tagline: null, category: null, logoUrl: null, updatedAt: UPDATED, deployCount: 0, totalProjects: 0, + ...over, +} as TemplateDraftSummary) + type Call = { method: string; path: string; body: unknown } type Answer = { status?: number; body: unknown } @@ -102,8 +112,15 @@ describe('draft rendering', () => { expect(templateStatusWord(view({ status: 'published', hasUnpublishedChanges: true }))).toBe('published, with unpublished edits') expect(templateStatusWord(view({ status: 'published', takenDown: true }))).toBe('taken down') }) - it('lists the org in aligned columns, and says so when there is nothing', () => { - expect(draftListLines([view(), view({ code: 'n8n-fork', name: 'n8n fork', status: 'published', publishedVersion: '1.0.2' })])).toEqual([ + it("has the platform's 16 summary keys in the summary fixture, no more and no fewer", () => { + const KEYS = [ + 'id', 'code', 'orgId', 'sourceProjectId', 'blank', 'status', 'publishedVersion', 'hasUnpublishedChanges', 'takenDown', + 'name', 'tagline', 'category', 'logoUrl', 'updatedAt', 'deployCount', 'totalProjects', + ] + expect(Object.keys(summary()).sort()).toEqual([...KEYS].sort()) + }) + it('lists the org in aligned columns from summary rows, and says so when there is nothing', () => { + expect(draftListLines([summary(), summary({ code: 'n8n-fork', name: 'n8n fork', status: 'published', publishedVersion: '1.0.2', hasUnpublishedChanges: false })])).toEqual([ 'CODE STATUS VERSION NAME', 'my-app draft - My App', 'n8n-fork published 1.0.2 n8n fork', @@ -157,7 +174,7 @@ describe('draft rendering', () => { describe('template drafts', () => { it("lists the linked project's org", async () => { const project = linked() - const { api, calls } = platform({ [`GET /orgs/${ORG}/templates`]: () => ({ body: { templates: [view()] } }) }) + const { api, calls } = platform({ [`GET /orgs/${ORG}/templates`]: () => ({ body: { templates: [summary()] } }) }) await templateDrafts({}, { api, project }) expect(calls.map((c) => `${c.method} ${c.path}`)).toEqual([`GET /orgs/${ORG}/templates`]) expect(printed()).toBe('CODE STATUS VERSION NAME\nmy-app draft - My App\n') @@ -169,8 +186,8 @@ describe('template drafts', () => { expect(project).not.toHaveBeenCalled() expect(calls[0]!.path).toBe(`/orgs/${OTHER_ORG}/templates`) }) - it('prints the platform list as served under --json', async () => { - const templates = [view()] + it('prints the platform list as served under --json, one summary per template', async () => { + const templates = [summary()] const { api } = platform({ [`GET /orgs/${ORG}/templates`]: () => ({ body: { templates } }) }) await templateDrafts({ json: true }, { api, project: linked() }) expect(JSON.parse(printed())).toEqual(templates) @@ -601,3 +618,15 @@ describe('template delete', () => { expect(e.message).toBe('a published template cannot be deleted, unpublish it instead') }) }) + +describe('template drafts --help', () => { + const entry = fileURLToPath(new URL('../src/index.ts', import.meta.url)) + it('says --json prints one summary per template and names the command that prints a draft in full', () => { + const r = spawnSync(process.execPath, ['--import', 'tsx', entry, 'template', 'drafts', '--help'], { encoding: 'utf8', timeout: 30_000 }) + expect(r.status, r.stderr).toBe(0) + // Commander wraps help at 80 columns, so words are matched across line breaks. + const help = r.stdout.replace(/\s+/g, ' ') + expect(help).toContain('one summary per template') + expect(help).toContain('insta template draft --json prints one draft in full') + }, 30_000) +})