@@ -4,16 +4,85 @@ import type {
44 INodeType ,
55 INodeTypeDescription ,
66} from 'n8n-workflow' ;
7- import { exec } from 'child_process' ;
8- import { promisify } from 'util' ;
97
10- const execAsync = promisify ( exec ) ;
8+ // Inlined spec data (no external dependencies)
9+ const requiredSpecs = [
10+ { file : '01-PRD.md' , name : 'Product Requirements Document (PRD)' } ,
11+ { file : '02-TECH-STACK.md' , name : 'Tech Stack' } ,
12+ { file : '03-DATABASE-SCHEMA.md' , name : 'Database Schema' } ,
13+ { file : '04-PROJECT-STRUCTURE.md' , name : 'Project Structure' } ,
14+ ] ;
1115
12- async function runSpacKit ( args : string ) : Promise < string > {
13- const { stdout } = await execAsync ( `npx -y @pirsquare.auto/spac-kit ${ args } ` , {
14- timeout : 60000 ,
15- } ) ;
16- return stdout . trim ( ) ;
16+ const optionalSpecs = [
17+ { file : '05-API-DESIGN.md' , name : 'API Design' } ,
18+ { file : '06-USER-STORIES.md' , name : 'User Stories' } ,
19+ { file : '07-ROADMAP.md' , name : 'Roadmap' } ,
20+ { file : '08-SITEMAP.md' , name : 'Sitemap' } ,
21+ ] ;
22+
23+ const projectTypes = [
24+ { value : 'fullstack' , name : 'Web App (Full-stack)' , description : 'General web app — Frontend + Backend + Database' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' , '08-SITEMAP.md' ] } ,
25+ { value : 'api' , name : 'API / Backend Service' , description : 'API-only — no frontend, focused on endpoints + database' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '07-ROADMAP.md' ] } ,
26+ { value : 'ecommerce' , name : 'E-commerce / Marketplace' , description : 'Online store — Products, Orders, Payments, Cart' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' , '08-SITEMAP.md' ] } ,
27+ { value : 'saas' , name : 'SaaS Platform' , description : 'Multi-tenant SaaS — Subscription, Billing, Teams' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' , '08-SITEMAP.md' ] } ,
28+ { value : 'mobile' , name : 'Mobile App' , description : 'Mobile app — React Native / Flutter + Backend API' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' ] } ,
29+ { value : 'landing' , name : 'Landing Page / Marketing Site' , description : 'Marketing site — lightweight backend, content + SEO focused' , defaultOptionalSpecs : [ '07-ROADMAP.md' , '08-SITEMAP.md' ] } ,
30+ { value : 'internal' , name : 'Internal Tool / Admin Dashboard' , description : 'Back-office system — CRUD, Reports, User Management' , defaultOptionalSpecs : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' ] } ,
31+ ] ;
32+
33+ const PACKAGES : Record < string , string > = {
34+ VERSION_NEXTJS : 'next' ,
35+ VERSION_TYPESCRIPT : 'typescript' ,
36+ VERSION_TAILWIND : 'tailwindcss' ,
37+ VERSION_SHADCN : 'shadcn' ,
38+ VERSION_ZUSTAND : 'zustand' ,
39+ VERSION_TANSTACK_QUERY : '@tanstack/react-query' ,
40+ VERSION_REACT_HOOK_FORM : 'react-hook-form' ,
41+ VERSION_ZOD : 'zod' ,
42+ VERSION_NESTJS : '@nestjs/core' ,
43+ VERSION_PRISMA : 'prisma' ,
44+ VERSION_PNPM : 'pnpm' ,
45+ VERSION_VITEST : 'vitest' ,
46+ VERSION_PLAYWRIGHT : '@playwright/test' ,
47+ VERSION_ESLINT : 'eslint' ,
48+ VERSION_PRETTIER : 'prettier' ,
49+ } ;
50+
51+ const FALLBACK_VERSIONS : Record < string , string > = {
52+ VERSION_NEXTJS : '15' , VERSION_TYPESCRIPT : '5.7' , VERSION_TAILWIND : '4' ,
53+ VERSION_SHADCN : '2' , VERSION_ZUSTAND : '5' , VERSION_TANSTACK_QUERY : '5' ,
54+ VERSION_REACT_HOOK_FORM : '7' , VERSION_ZOD : '3' , VERSION_NESTJS : '11' ,
55+ VERSION_PRISMA : '6' , VERSION_PNPM : '9' , VERSION_VITEST : '3' ,
56+ VERSION_PLAYWRIGHT : '1' , VERSION_ESLINT : '9' , VERSION_PRETTIER : '3' ,
57+ VERSION_NODE : '22' , VERSION_POSTGRESQL : '17' , VERSION_REDIS : '7' ,
58+ } ;
59+
60+ async function fetchLatestVersions ( ) : Promise < Record < string , string > > {
61+ const versions = { ...FALLBACK_VERSIONS } ;
62+ const USE_MINOR = new Set ( [ 'typescript' , 'tailwindcss' ] ) ;
63+
64+ const results = await Promise . allSettled (
65+ Object . entries ( PACKAGES ) . map ( async ( [ key , pkg ] ) => {
66+ const res = await fetch ( `https://registry.npmjs.org/${ pkg } /latest` , {
67+ signal : AbortSignal . timeout ( 5000 ) ,
68+ } ) ;
69+ if ( ! res . ok ) return [ key , null ] as const ;
70+ const data = await res . json ( ) as { version : string } ;
71+ const parts = data . version . split ( '.' ) ;
72+ const ver = ( parseInt ( parts [ 0 ] ) === 0 || USE_MINOR . has ( pkg ) )
73+ ? `${ parts [ 0 ] } .${ parts [ 1 ] } `
74+ : parts [ 0 ] ;
75+ return [ key , ver ] as const ;
76+ } ) ,
77+ ) ;
78+
79+ for ( const result of results ) {
80+ if ( result . status === 'fulfilled' && result . value [ 1 ] ) {
81+ versions [ result . value [ 0 ] ] = result . value [ 1 ] ;
82+ }
83+ }
84+
85+ return versions ;
1786}
1887
1988export class SpacKit implements INodeType {
@@ -38,12 +107,6 @@ export class SpacKit implements INodeType {
38107 type : 'options' ,
39108 noDataExpression : true ,
40109 options : [
41- {
42- name : 'Create Project' ,
43- value : 'createProject' ,
44- description : 'Create a new project with spec templates' ,
45- action : 'Create a new project with spec templates' ,
46- } ,
47110 {
48111 name : 'Get Presets' ,
49112 value : 'getPresets' ,
@@ -56,28 +119,20 @@ export class SpacKit implements INodeType {
56119 description : 'Fetch latest npm package versions' ,
57120 action : 'Fetch latest npm package versions' ,
58121 } ,
59- ] ,
60- default : 'createProject' ,
61- } ,
62- {
63- displayName : 'Project Name' ,
64- name : 'projectName' ,
65- type : 'string' ,
66- default : '' ,
67- required : true ,
68- displayOptions : {
69- show : {
70- operation : [ 'createProject' ] ,
122+ {
123+ name : 'Get Spec List' ,
124+ value : 'getSpecList' ,
125+ description : 'Get all spec files for a preset' ,
126+ action : 'Get all spec files for a preset' ,
71127 } ,
72- } ,
73- description : 'Name of the project (lowercase, a-z, 0-9, hyphens, underscores) ' ,
128+ ] ,
129+ default : 'getPresets ' ,
74130 } ,
75131 {
76132 displayName : 'Preset' ,
77133 name : 'preset' ,
78134 type : 'options' ,
79135 options : [
80- { name : 'General (No Preset)' , value : '' } ,
81136 { name : 'Web App (Full-stack)' , value : 'fullstack' } ,
82137 { name : 'API / Backend Service' , value : 'api' } ,
83138 { name : 'E-commerce / Marketplace' , value : 'ecommerce' } ,
@@ -89,53 +144,11 @@ export class SpacKit implements INodeType {
89144 default : 'fullstack' ,
90145 displayOptions : {
91146 show : {
92- operation : [ 'createProject ' ] ,
147+ operation : [ 'getSpecList ' ] ,
93148 } ,
94149 } ,
95150 description : 'Project type preset' ,
96151 } ,
97- {
98- displayName : 'Target Directory' ,
99- name : 'targetDir' ,
100- type : 'string' ,
101- default : '/tmp' ,
102- displayOptions : {
103- show : {
104- operation : [ 'createProject' ] ,
105- } ,
106- } ,
107- description : 'Directory to create the project in' ,
108- } ,
109- {
110- displayName : 'Overwrite' ,
111- name : 'overwrite' ,
112- type : 'boolean' ,
113- default : false ,
114- displayOptions : {
115- show : {
116- operation : [ 'createProject' ] ,
117- } ,
118- } ,
119- description : 'Whether to overwrite existing spac/ directory' ,
120- } ,
121- {
122- displayName : 'Optional Specs' ,
123- name : 'optionalSpecs' ,
124- type : 'multiOptions' ,
125- options : [
126- { name : 'API Design' , value : '05-API-DESIGN.md' } ,
127- { name : 'User Stories' , value : '06-USER-STORIES.md' } ,
128- { name : 'Roadmap' , value : '07-ROADMAP.md' } ,
129- { name : 'Sitemap' , value : '08-SITEMAP.md' } ,
130- ] ,
131- default : [ '05-API-DESIGN.md' , '06-USER-STORIES.md' , '07-ROADMAP.md' , '08-SITEMAP.md' ] ,
132- displayOptions : {
133- show : {
134- operation : [ 'createProject' ] ,
135- } ,
136- } ,
137- description : 'Optional spec files to include' ,
138- } ,
139152 ] ,
140153 } ;
141154
@@ -146,58 +159,34 @@ export class SpacKit implements INodeType {
146159
147160 for ( let i = 0 ; i < items . length ; i ++ ) {
148161 try {
149- if ( operation === 'createProject' ) {
150- const projectName = this . getNodeParameter ( 'projectName' , i ) as string ;
151- const preset = this . getNodeParameter ( 'preset' , i ) as string ;
152- const targetDir = this . getNodeParameter ( 'targetDir' , i ) as string ;
153- const overwrite = this . getNodeParameter ( 'overwrite' , i ) as boolean ;
154- const optionalSpecs = this . getNodeParameter ( 'optionalSpecs' , i ) as string [ ] ;
155-
156- // Use Node.js API via inline script
157- const script = `
158- const m = await import("@pirsquare.auto/spac-kit");
159- const r = await m.createProject(${ JSON . stringify ( projectName ) } , {
160- preset: ${ JSON . stringify ( preset || undefined ) } ,
161- optionalSpecs: ${ JSON . stringify ( optionalSpecs ) } ,
162- overwrite: ${ overwrite } ,
163- targetDir: ${ JSON . stringify ( targetDir ) } ,
164- });
165- process.stdout.write(JSON.stringify({ success: true, projectDir: r.projectDir, spacDir: r.spacDir, files: r.files, fileCount: r.files.length }));
166- ` . trim ( ) ;
167-
168- const { stdout } = await execAsync (
169- `npx -y @pirsquare.auto/spac-kit node -e '${ script . replace ( / ' / g, "'\\''" ) } '` ,
170- { timeout : 120000 } ,
171- ) . catch ( async ( ) => {
172- // Fallback: use node directly with dynamic import
173- return execAsync (
174- `node --input-type=module -e "${ script . replace ( / " / g, '\\"' ) } "` ,
175- { timeout : 120000 , env : { ...process . env , NODE_PATH : '' } } ,
176- ) ;
162+ if ( operation === 'getPresets' ) {
163+ returnData . push ( {
164+ json : {
165+ presets : projectTypes ,
166+ requiredSpecs,
167+ optionalSpecs,
168+ installCommand : 'npx @pirsquare.auto/spac-kit init' ,
169+ } ,
177170 } ) ;
178-
179- returnData . push ( { json : JSON . parse ( stdout ) } ) ;
180- } else if ( operation === 'getPresets' ) {
181- const script = `
182- import{getPresets as a,getRequiredSpecs as b,getOptionalSpecs as c}from"@pirsquare.auto/spac-kit";
183- process.stdout.write(JSON.stringify({presets:a(),requiredSpecs:b(),optionalSpecs:c()}));
184- ` . trim ( ) ;
185- const { stdout } = await execAsync (
186- `node --input-type=module -e "${ script . replace ( / " / g, '\\"' ) } "` ,
187- { timeout : 60000 } ,
188- ) ;
189- returnData . push ( { json : JSON . parse ( stdout ) } ) ;
190171 } else if ( operation === 'getVersions' ) {
191- const script = `
192- import{fetchLatestVersions}from"@pirsquare.auto/spac-kit/versions";
193- const v=await fetchLatestVersions();
194- process.stdout.write(JSON.stringify(v));
195- ` . trim ( ) ;
196- const { stdout } = await execAsync (
197- `node --input-type=module -e "${ script . replace ( / " / g, '\\"' ) } "` ,
198- { timeout : 60000 } ,
199- ) ;
200- returnData . push ( { json : JSON . parse ( stdout ) } ) ;
172+ const versions = await fetchLatestVersions ( ) ;
173+ returnData . push ( { json : versions } ) ;
174+ } else if ( operation === 'getSpecList' ) {
175+ const presetValue = this . getNodeParameter ( 'preset' , i ) as string ;
176+ const preset = projectTypes . find ( ( p ) => p . value === presetValue ) ;
177+ const allFiles = [
178+ '00-SCOPE-OF-WORK.md' ,
179+ ...requiredSpecs . map ( ( s ) => s . file ) ,
180+ ...( preset ? preset . defaultOptionalSpecs : optionalSpecs . map ( ( s ) => s . file ) ) ,
181+ ] ;
182+ returnData . push ( {
183+ json : {
184+ preset : preset ?. name ?? 'Unknown' ,
185+ files : allFiles ,
186+ fileCount : allFiles . length ,
187+ createCommand : `npx @pirsquare.auto/spac-kit init my-project` ,
188+ } ,
189+ } ) ;
201190 }
202191 } catch ( error : unknown ) {
203192 if ( this . continueOnFail ( ) ) {
0 commit comments