Skip to content

Commit 23d4409

Browse files
Ohm-Kasinbadeeclaude
authored andcommitted
fix(n8n): inline all logic, zero dependencies, n8n Cloud compatible
- Remove child_process, util, process usage - Inline preset data and version fetcher - Replace Create Project with Get Spec List (n8n Cloud has no filesystem) - Use global fetch() for npm registry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1d2aee1 commit 23d4409

3 files changed

Lines changed: 1314 additions & 123 deletions

File tree

‎n8n-nodes-spac-kit/nodes/SpacKit/SpacKit.node.ts‎

Lines changed: 111 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1988
export 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

Comments
 (0)