@heyputer/shell 2.1.0
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/.claude/settings.local.json +9 -0
- package/.env.example +3 -0
- package/.github/workflows/npm-build.yml +34 -0
- package/.github/workflows/npm-publish.yml +50 -0
- package/.idx/dev.nix +53 -0
- package/CHANGELOG.md +292 -0
- package/LICENSE.md +21 -0
- package/README.md +18 -0
- package/bin/index.js +258 -0
- package/package.json +58 -0
- package/screenshot.png +0 -0
- package/src/commands/apps.js +356 -0
- package/src/commands/auth.js +195 -0
- package/src/commands/deploy.js +54 -0
- package/src/commands/files.js +1187 -0
- package/src/commands/init.js +322 -0
- package/src/commands/shell.js +61 -0
- package/src/commands/sites.js +169 -0
- package/src/commands/subdomains.js +95 -0
- package/src/commons.js +409 -0
- package/src/crypto.js +9 -0
- package/src/executor.js +386 -0
- package/src/modules/ErrorModule.js +20 -0
- package/src/modules/ProfileModule.js +334 -0
- package/src/modules/PuterModule.js +30 -0
- package/src/utils.js +135 -0
- package/tests/ErrorModule.test.js +42 -0
- package/tests/ProfileModule.test.js +274 -0
- package/tests/PuterModule.test.js +56 -0
- package/tests/apps.test.js +194 -0
- package/tests/commons.test.js +380 -0
- package/tests/deploy.test.js +84 -0
- package/tests/executor.test.js +52 -0
- package/tests/files.test.js +640 -0
- package/tests/login.test.js +206 -0
- package/tests/shell.test.js +184 -0
- package/tests/sites.test.js +67 -0
- package/tests/subdomains.test.js +90 -0
- package/tests/utils.test.js +193 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { generateAppName, getDefaultHomePage } from '../commons.js';
|
|
7
|
+
import { getProfileModule } from '../modules/ProfileModule.js';
|
|
8
|
+
|
|
9
|
+
const JS_BUNDLERS = ['Vite', 'Webpack', 'Parcel', 'esbuild', 'Farm'];
|
|
10
|
+
const FULLSTACK_FRAMEWORKS = ['Next', 'Nuxt', 'SvelteKit', 'Astro'];
|
|
11
|
+
const JS_LIBRARIES = ['React', 'Vue', 'Angular', 'Svelte', 'jQuery'];
|
|
12
|
+
const CSS_LIBRARIES = ['Bootstrap', 'Bulma', 'shadcn', 'Tailwind', 'Material-UI', 'Semantic UI', 'AntDesign', 'Element-Plus', 'PostCSS', 'AutoPrefixer'];
|
|
13
|
+
|
|
14
|
+
export async function init() {
|
|
15
|
+
const profileModule = getProfileModule();
|
|
16
|
+
await profileModule.checkLogin();
|
|
17
|
+
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'input',
|
|
21
|
+
name: 'name',
|
|
22
|
+
message: 'What is your app name?',
|
|
23
|
+
default: `${generateAppName()}`
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'list',
|
|
27
|
+
name: 'useBundler',
|
|
28
|
+
message: 'Do you want to use a JavaScript bundler?',
|
|
29
|
+
choices: ['Yes', 'No (Use CDN)']
|
|
30
|
+
}
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
let jsFiles = [];
|
|
34
|
+
let jsDevFiles = [];
|
|
35
|
+
let cssFiles = [];
|
|
36
|
+
let jsExtraLibraries = [];
|
|
37
|
+
let extraFiles = [];
|
|
38
|
+
let bundlerAnswers = null;
|
|
39
|
+
let frameworkAnswers = null;
|
|
40
|
+
|
|
41
|
+
if (answers.useBundler === 'Yes') {
|
|
42
|
+
bundlerAnswers = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
type: 'list',
|
|
45
|
+
name: 'bundler',
|
|
46
|
+
message: 'Select a JavaScript bundler:',
|
|
47
|
+
choices: JS_BUNDLERS
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'list',
|
|
51
|
+
name: 'frameworkType',
|
|
52
|
+
message: 'Do you want to use a full-stack framework or custom libraries?',
|
|
53
|
+
choices: ['Full-stack framework', 'Custom libraries']
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
if (bundlerAnswers.frameworkType === 'Full-stack framework') {
|
|
58
|
+
frameworkAnswers = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'framework',
|
|
62
|
+
message: 'Select a full-stack framework:',
|
|
63
|
+
choices: FULLSTACK_FRAMEWORKS
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
switch (frameworkAnswers.framework) {
|
|
68
|
+
case FULLSTACK_FRAMEWORKS[0]:
|
|
69
|
+
jsFiles.push('next@latest');
|
|
70
|
+
extraFiles.push({
|
|
71
|
+
path: 'src/index.tsx',
|
|
72
|
+
content: `export default function Home() { return (<h1>${answers.name}</h1>) }`
|
|
73
|
+
});
|
|
74
|
+
break;
|
|
75
|
+
case FULLSTACK_FRAMEWORKS[1]:
|
|
76
|
+
jsFiles.push('nuxt@latest');
|
|
77
|
+
extraFiles.push({
|
|
78
|
+
path: 'src/app.vue',
|
|
79
|
+
content: `<template><h1>${answers.name}</h1></template>`
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
case FULLSTACK_FRAMEWORKS[2]:
|
|
83
|
+
jsFiles.push('svelte@latest', 'sveltekit@latest');
|
|
84
|
+
extraFiles.push({
|
|
85
|
+
path: 'src/app.vue',
|
|
86
|
+
content: `<template><h1>${answers.name}</h1></template>`
|
|
87
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
case FULLSTACK_FRAMEWORKS[3]:
|
|
90
|
+
jsFiles.push('astro@latest', 'astro@latest');
|
|
91
|
+
extraFiles.push({
|
|
92
|
+
path: 'src/pages/index.astro',
|
|
93
|
+
content: `---\n\n<Layout title="Welcome to ${answers.name}."><h1>${answers.name}</h1></Layout>`
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const libraryAnswers = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'list',
|
|
101
|
+
name: 'library',
|
|
102
|
+
message: 'Select a JavaScript library/framework:',
|
|
103
|
+
choices: JS_LIBRARIES
|
|
104
|
+
}
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
switch (libraryAnswers.library) {
|
|
108
|
+
case JS_LIBRARIES[0]:
|
|
109
|
+
jsFiles.push('react@latest', 'react-dom@latest');
|
|
110
|
+
const reactLibs = await inquirer.prompt([
|
|
111
|
+
{
|
|
112
|
+
type: 'checkbox',
|
|
113
|
+
name: 'reactLibraries',
|
|
114
|
+
message: 'Select React libraries:',
|
|
115
|
+
choices: CSS_LIBRARIES.concat(['react-router-dom', 'react-redux', 'react-bootstrap', '@chakra-ui/react', 'semantic-ui-react'])
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
jsFiles.push(...reactLibs.reactLibraries);
|
|
119
|
+
extraFiles.push({
|
|
120
|
+
path: 'src/App.jsx',
|
|
121
|
+
content: `export default function Home() { return (<h1>${answers.name}</h1>) }`
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case JS_LIBRARIES[1]:
|
|
125
|
+
jsFiles.push('vue@latest');
|
|
126
|
+
jsDevFiles.push('@vitejs/plugin-vue');
|
|
127
|
+
const vueLibs = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
type: 'checkbox',
|
|
130
|
+
name: 'vueLibraries',
|
|
131
|
+
message: 'Select Vue libraries:',
|
|
132
|
+
choices: CSS_LIBRARIES.concat(['shadcn-vue', 'UnoCSS', 'NaiveUI', 'bootstrap-vue-next', 'buefy', 'vue-router', 'pinia'])
|
|
133
|
+
}
|
|
134
|
+
]);
|
|
135
|
+
jsFiles.push(...vueLibs.vueLibraries);
|
|
136
|
+
extraFiles.push(
|
|
137
|
+
{
|
|
138
|
+
path: 'src/App.vue',
|
|
139
|
+
content: `<template><h1>${answers.name}</h1></template>`
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: 'vite.config.js',
|
|
143
|
+
content: `import { defineConfig } from 'vite';
|
|
144
|
+
import vue from '@vitejs/plugin-vue';
|
|
145
|
+
|
|
146
|
+
export default defineConfig({
|
|
147
|
+
plugins: [vue()]
|
|
148
|
+
})
|
|
149
|
+
`},
|
|
150
|
+
{
|
|
151
|
+
path: 'main.js',
|
|
152
|
+
content: `import { createApp } from 'vue'
|
|
153
|
+
import './style.css';
|
|
154
|
+
import App from './App.vue';
|
|
155
|
+
|
|
156
|
+
const app = createApp(App);
|
|
157
|
+
app.mount('#app');
|
|
158
|
+
`},
|
|
159
|
+
);
|
|
160
|
+
break;
|
|
161
|
+
case JS_LIBRARIES[2]:
|
|
162
|
+
jsFiles.push('@angular/core@latest');
|
|
163
|
+
extraFiles.push({
|
|
164
|
+
path: 'src/index.controller.js',
|
|
165
|
+
content: `(function () { angular.module('app', [])})`
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
case JS_LIBRARIES[3]:
|
|
169
|
+
jsFiles.push('svelte@latest');
|
|
170
|
+
break;
|
|
171
|
+
case JS_LIBRARIES[4]:
|
|
172
|
+
jsFiles.push('jquery@latest');
|
|
173
|
+
extraFiles.push({
|
|
174
|
+
path: 'src/main.js',
|
|
175
|
+
content: `$(function(){})`
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
|
|
182
|
+
const cdnAnswers = await inquirer.prompt([
|
|
183
|
+
{
|
|
184
|
+
type: 'list',
|
|
185
|
+
name: 'jsFramework',
|
|
186
|
+
message: 'Select a JavaScript framework/library (CDN):',
|
|
187
|
+
choices: JS_LIBRARIES
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'list',
|
|
191
|
+
name: 'cssFramework',
|
|
192
|
+
message: 'Select a CSS framework/library (CDN):',
|
|
193
|
+
choices: CSS_LIBRARIES //'Tailwind', 'Bootstrap', 'Bulma'...
|
|
194
|
+
}
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
switch (cdnAnswers.jsFramework) {
|
|
198
|
+
case JS_LIBRARIES[0]:
|
|
199
|
+
jsFiles.push('https://unpkg.com/react@latest/umd/react.production.min.js');
|
|
200
|
+
jsFiles.push('https://unpkg.com/react-dom@latest/umd/react-dom.production.min.js');
|
|
201
|
+
break;
|
|
202
|
+
case JS_LIBRARIES[1]:
|
|
203
|
+
jsFiles.push('https://unpkg.com/vue@latest/dist/vue.global.js');
|
|
204
|
+
break;
|
|
205
|
+
case JS_LIBRARIES[2]:
|
|
206
|
+
jsFiles.push('https://unpkg.com/@angular/core@latest/bundles/core.umd.js');
|
|
207
|
+
break;
|
|
208
|
+
case JS_LIBRARIES[3]:
|
|
209
|
+
jsFiles.push('https://unpkg.com/svelte@latest/compiled/svelte.js');
|
|
210
|
+
break;
|
|
211
|
+
case JS_LIBRARIES[4]:
|
|
212
|
+
jsFiles.push('https://code.jquery.com/jquery-latest.min.js');
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
switch (cdnAnswers.cssFramework) {
|
|
217
|
+
case CSS_LIBRARIES[0]:
|
|
218
|
+
cssFiles.push('https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css');
|
|
219
|
+
break;
|
|
220
|
+
case CSS_LIBRARIES[1]:
|
|
221
|
+
cssFiles.push('https://cdn.jsdelivr.net/npm/bulma@latest/css/bulma.min.css');
|
|
222
|
+
break;
|
|
223
|
+
case CSS_LIBRARIES[2]:
|
|
224
|
+
cssFiles.push('https://cdn.tailwindcss.com');
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
const spinner = ora('Creating Puter app...').start();
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const useBundler = answers.useBundler === 'Yes';
|
|
234
|
+
// Create basic app structure
|
|
235
|
+
await createAppStructure(answers.name, useBundler, bundlerAnswers, frameworkAnswers, jsFiles, jsDevFiles, cssFiles, extraFiles);
|
|
236
|
+
spinner.succeed(chalk.green('Successfully created Puter app!'));
|
|
237
|
+
|
|
238
|
+
console.log('\nNext steps:');
|
|
239
|
+
console.log(chalk.cyan('1. cd'), answers.name);
|
|
240
|
+
if (useBundler) {
|
|
241
|
+
console.log(chalk.cyan('2. npm install'));
|
|
242
|
+
console.log(chalk.cyan('3. npm start'));
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.cyan('2. Open index.html in your browser'));
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
spinner.fail(chalk.red('Failed to create app'));
|
|
248
|
+
console.error(error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function createAppStructure(name, useBundler, bundlerAnswers, frameworkAnswers, jsFiles, jsDevFiles, cssFiles, extraFiles) {
|
|
253
|
+
// Create project directory
|
|
254
|
+
await fs.mkdir(name, { recursive: true });
|
|
255
|
+
|
|
256
|
+
// Generate default home page
|
|
257
|
+
const homePage = useBundler?getDefaultHomePage(name): getDefaultHomePage(name, jsFiles, cssFiles);
|
|
258
|
+
|
|
259
|
+
// Create basic files
|
|
260
|
+
const files = {
|
|
261
|
+
'.env': `APP_NAME=${name}\nPUTER_API_KEY=`,
|
|
262
|
+
'index.html': homePage,
|
|
263
|
+
'styles.css': `body {
|
|
264
|
+
font-family: 'Segoe UI', Roboto, sans-serif;
|
|
265
|
+
margin: 0 auto;
|
|
266
|
+
padding: 10px;
|
|
267
|
+
}`,
|
|
268
|
+
'app.js': `// Initialize Puter app
|
|
269
|
+
console.log('Puter app initialized!');`,
|
|
270
|
+
'README.md': `# ${name}\n\nA Puter app created with puter-cli`
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
274
|
+
await fs.writeFile(path.join(name, filename), content);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If using a bundler, create a package.json
|
|
278
|
+
// if (jsFiles.some(file => !file.startsWith('http'))) {
|
|
279
|
+
if (useBundler) {
|
|
280
|
+
|
|
281
|
+
const useFullStackFramework = bundlerAnswers.frameworkType === 'Full-stack framework';
|
|
282
|
+
const bundler = bundlerAnswers.bundler.toString().toLowerCase();
|
|
283
|
+
const framework = useFullStackFramework?frameworkAnswers.framework.toLowerCase():null;
|
|
284
|
+
|
|
285
|
+
const scripts = {
|
|
286
|
+
start: `${useFullStackFramework?`${framework} dev`:bundler} dev`,
|
|
287
|
+
build: `${useFullStackFramework?`${framework} build`:bundler} build`,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const packageJson = {
|
|
291
|
+
name: name,
|
|
292
|
+
version: '1.0.0',
|
|
293
|
+
type: 'module',
|
|
294
|
+
scripts,
|
|
295
|
+
dependencies: {},
|
|
296
|
+
devDependencies: {}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
jsFiles.forEach(lib => {
|
|
301
|
+
if (!lib.startsWith('http')) {
|
|
302
|
+
packageJson.dependencies[lib.split('@')[0].toString().toLowerCase()] = lib.split('@')[1] || 'latest';
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
jsDevFiles.forEach(lib => {
|
|
307
|
+
packageJson.devDependencies[lib] = 'latest';
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
packageJson.devDependencies[bundler] = 'latest';
|
|
311
|
+
|
|
312
|
+
await fs.writeFile(path.join(name, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
313
|
+
|
|
314
|
+
extraFiles.forEach(async (extraFile) => {
|
|
315
|
+
const fullPath = path.join(name, extraFile.path);
|
|
316
|
+
// Create directories recursively if they don't exist
|
|
317
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
318
|
+
await fs.writeFile(fullPath, extraFile.content);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Conf from 'conf';
|
|
4
|
+
import { execCommand, getPrompt } from '../executor.js';
|
|
5
|
+
import { PROJECT_NAME } from '../commons.js';
|
|
6
|
+
import { getProfileModule } from '../modules/ProfileModule.js';
|
|
7
|
+
|
|
8
|
+
const config = new Conf({ projectName: PROJECT_NAME });
|
|
9
|
+
|
|
10
|
+
export let rl;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Update the current shell prompt
|
|
14
|
+
*/
|
|
15
|
+
export function updatePrompt(currentPath) {
|
|
16
|
+
config.set('cwd', currentPath);
|
|
17
|
+
rl.setPrompt(getPrompt());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start the interactive shell
|
|
22
|
+
*/
|
|
23
|
+
export async function startShell(command) {
|
|
24
|
+
const profileModule = getProfileModule();
|
|
25
|
+
await profileModule.checkLogin();
|
|
26
|
+
|
|
27
|
+
// This argument enables the `puter <subcommand>` commands
|
|
28
|
+
if (command) {
|
|
29
|
+
await execCommand(command);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
prompt: null
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
console.log(chalk.green('Welcome to Puter-CLI! Type "help" for available commands.'));
|
|
41
|
+
rl.setPrompt(getPrompt());
|
|
42
|
+
rl.prompt();
|
|
43
|
+
|
|
44
|
+
rl.on('line', async (line) => {
|
|
45
|
+
const trimmedLine = line.trim();
|
|
46
|
+
if (trimmedLine) {
|
|
47
|
+
try {
|
|
48
|
+
await execCommand(trimmedLine);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(chalk.red(error.message));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
rl.prompt();
|
|
54
|
+
}).on('close', () => {
|
|
55
|
+
console.log(chalk.yellow('\nGoodbye!'));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(chalk.red('Error starting shell:', error));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { getCurrentUserName, getCurrentDirectory } from './auth.js';
|
|
4
|
+
import { resolveRemotePath, isValidAppName } from '../commons.js';
|
|
5
|
+
import { displayNonNullValues, formatDate, isValidAppUuid } from '../utils.js';
|
|
6
|
+
import { getSubdomains, createSubdomain, deleteSubdomain, updateSubdomain } from './subdomains.js';
|
|
7
|
+
import { getPuter } from '../modules/PuterModule.js';
|
|
8
|
+
import { report } from '../modules/ErrorModule.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Listing subdomains
|
|
12
|
+
*/
|
|
13
|
+
export async function listSites(args = {}) {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getSubdomains(args);
|
|
16
|
+
|
|
17
|
+
// Create table instance
|
|
18
|
+
const table = new Table({
|
|
19
|
+
head: [
|
|
20
|
+
chalk.cyan('#'),
|
|
21
|
+
chalk.cyan('UID'),
|
|
22
|
+
chalk.cyan('Subdomain'),
|
|
23
|
+
chalk.cyan('Created'),
|
|
24
|
+
chalk.cyan('Protected'),
|
|
25
|
+
// chalk.cyan('Owner'),
|
|
26
|
+
chalk.cyan('Directory')
|
|
27
|
+
],
|
|
28
|
+
wordWrap: false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Format and add data to table
|
|
32
|
+
let i = 0;
|
|
33
|
+
result.forEach(domain => {
|
|
34
|
+
let appDir = domain?.root_dir?.path.split('/').pop().split('-');
|
|
35
|
+
table.push([
|
|
36
|
+
i++,
|
|
37
|
+
domain.uid,
|
|
38
|
+
chalk.green(`${chalk.dim(domain.subdomain)}.puter.site`),
|
|
39
|
+
formatDate(domain.created_at).split(',')[0],
|
|
40
|
+
domain.protected ? chalk.red('Yes') : chalk.green('No'),
|
|
41
|
+
// domain.owner['username'],
|
|
42
|
+
appDir && (isValidAppUuid(appDir.join('-'))?`${appDir[0]}-...-${appDir.slice(-1)}`:appDir.join('-'))
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Print table
|
|
47
|
+
if (result.length === 0) {
|
|
48
|
+
console.log(chalk.yellow('No subdomains found'));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.bold('\nYour Sites:'));
|
|
51
|
+
console.log(table.toString());
|
|
52
|
+
console.log(chalk.dim(`Total Sites: ${result.length}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
report(error);
|
|
57
|
+
console.error(chalk.red('Error listing sites:'), error.message);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get Site info
|
|
64
|
+
* @param {any[]} args Array of site uuid
|
|
65
|
+
*/
|
|
66
|
+
export async function infoSite(args = []) {
|
|
67
|
+
if (args.length < 1){
|
|
68
|
+
console.log(chalk.red('Usage: site <siteUID>'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const puter = getPuter();
|
|
72
|
+
for (const subdomain of args)
|
|
73
|
+
try {
|
|
74
|
+
const result = await puter.hosting.get(subdomain);
|
|
75
|
+
displayNonNullValues(result);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(chalk.red('Error getting site info:'), error.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delete hosted web site
|
|
83
|
+
* @param {any[]} args Array of subdomain
|
|
84
|
+
*/
|
|
85
|
+
export async function deleteSite(args = []) {
|
|
86
|
+
if (args.length < 1){
|
|
87
|
+
console.log(chalk.red('Usage: site:delete <subdomain>'));
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
await deleteSubdomain(args);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a static web app from the current directory to Puter cloud.
|
|
96
|
+
* @param {string[]} args - Command-line arguments (e.g., [name, --subdomain=<subdomain>]).
|
|
97
|
+
*/
|
|
98
|
+
export async function createSite(args = []) {
|
|
99
|
+
if (args.length < 1 || !isValidAppName(args[0])) {
|
|
100
|
+
console.log(chalk.red('Usage: site:create <valid_name_app> [<remote_dir>] [--subdomain=<subdomain>]'));
|
|
101
|
+
console.log(chalk.yellow('Example: site:create mysite'));
|
|
102
|
+
console.log(chalk.yellow('Example: site:create mysite ./mysite'));
|
|
103
|
+
console.log(chalk.yellow('Example: site:create mysite --subdomain=mysite'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const appName = args[0]; // Site name (required)
|
|
108
|
+
const subdomainOption = args.find(arg => arg.toLocaleLowerCase().startsWith('--subdomain='))?.split('=')[1]; // Optional subdomain
|
|
109
|
+
const remoteDirArg = (args[1] && !args[1].startsWith('--')) ? args[1] : '.';
|
|
110
|
+
|
|
111
|
+
// Use the current directory as the root directory if none specified
|
|
112
|
+
const remoteDir = resolveRemotePath(getCurrentDirectory(), remoteDirArg);
|
|
113
|
+
|
|
114
|
+
console.log(chalk.dim(`Creating site ${chalk.green(appName)} from: ${chalk.green(remoteDir)}...\n`));
|
|
115
|
+
try {
|
|
116
|
+
// Step 1: Determine the subdomain
|
|
117
|
+
let subdomain;
|
|
118
|
+
if (subdomainOption) {
|
|
119
|
+
subdomain = subdomainOption; // Use the provided subdomain
|
|
120
|
+
} else {
|
|
121
|
+
subdomain = appName; // Default to the app name as the subdomain
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 2: Check if the subdomain already exists
|
|
125
|
+
const subdomains = await getSubdomains();;
|
|
126
|
+
const subdomainObj = subdomains.find(sd => sd.subdomain === subdomain);
|
|
127
|
+
if (subdomainObj) {
|
|
128
|
+
console.error(chalk.cyan(`The subdomain "${subdomain}" is already in use and owned by: "${subdomainObj.owner['username']}"`));
|
|
129
|
+
if (subdomainObj.owner['username'] === getCurrentUserName()){
|
|
130
|
+
console.log(chalk.green(`It's yours, and linked to: ${subdomainObj.root_dir?.path}`));
|
|
131
|
+
if (subdomainObj.root_dir?.path === remoteDir){
|
|
132
|
+
console.log(chalk.cyan(`Which is already the selected directory, and created at:`));
|
|
133
|
+
console.log(chalk.green(`https://${subdomain}.puter.site`));
|
|
134
|
+
return;
|
|
135
|
+
} else {
|
|
136
|
+
console.log(chalk.yellow(`However, It's linked to different directory at: ${subdomainObj.root_dir?.path}`));
|
|
137
|
+
console.log(chalk.cyan(`Updating this subdomain directory...`));
|
|
138
|
+
const result = await updateSubdomain(subdomain, remoteDir);
|
|
139
|
+
if (result) {
|
|
140
|
+
console.log(chalk.green('Updating subdomain directory successful.'));
|
|
141
|
+
return;
|
|
142
|
+
} else {
|
|
143
|
+
console.log(chalk.red('Could not update this subdomain directory.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Use the chosen "subdomain"
|
|
151
|
+
console.log(chalk.cyan(`New generated subdomain: "${subdomain}" will be used if its not already in use.`));
|
|
152
|
+
|
|
153
|
+
// Step 3: Host the current directory under the subdomain
|
|
154
|
+
console.log(chalk.cyan(`Hosting site "${appName}" under subdomain "${subdomain}"...`));
|
|
155
|
+
const site = await createSubdomain(subdomain, remoteDir);
|
|
156
|
+
if (!site){
|
|
157
|
+
console.error(chalk.red(`Failed to create subdomain: "${chalk.red(subdomain)}"`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(chalk.green(`Site ${chalk.dim(appName)} created successfully and accessible at:`));
|
|
162
|
+
console.log(chalk.cyan(`https://${site.subdomain}.puter.site`));
|
|
163
|
+
return site;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(chalk.red('Failed to create site.'));
|
|
166
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import { API_BASE, getHeaders } from '../commons.js';
|
|
4
|
+
import { getPuter } from '../modules/PuterModule.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get list of subdomains.
|
|
8
|
+
* @param {Object} args - Options for the query.
|
|
9
|
+
* @returns {Array} - Array of subdomains.
|
|
10
|
+
*/
|
|
11
|
+
export async function getSubdomains(args = {}) {
|
|
12
|
+
const puter = getPuter();
|
|
13
|
+
let result;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
result = await puter.hosting.list();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.log(chalk.red(`Error when getting subdomains.\nError: ${error?.message}`));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Delete a subdomain by id
|
|
26
|
+
* @param {Array} subdomain IDs
|
|
27
|
+
* @return {boolean} Result of the operation
|
|
28
|
+
*/
|
|
29
|
+
export async function deleteSubdomain(args = []) {
|
|
30
|
+
if (args.length < 1){
|
|
31
|
+
console.log(chalk.red('Usage: domain:delete <subdomain_id>'));
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const puter = getPuter();
|
|
35
|
+
const subdomains = args;
|
|
36
|
+
for (const subdomain of subdomains)
|
|
37
|
+
try {
|
|
38
|
+
const success = await puter.hosting.delete(subdomain);
|
|
39
|
+
|
|
40
|
+
if (!success) {
|
|
41
|
+
console.log(chalk.red(`Failed to delete subdomain: ${data.error?.message}`));
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
console.log(chalk.green('Subdomain deleted successfully'));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.error?.code === 'entity_not_found') {
|
|
47
|
+
console.log(chalk.red(`Subdomain: "${subdomain}" not found`));
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
console.error(chalk.red('Error deleting subdomain:'), error.message);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new subdomain into remote directory
|
|
57
|
+
* @param {string} subdomain - Subdomain name.
|
|
58
|
+
* @param {string} remoteDir - Remote directory path.
|
|
59
|
+
* @returns {Object} - Hosting details (e.g., subdomain).
|
|
60
|
+
*/
|
|
61
|
+
export async function createSubdomain(subdomain, remoteDir) {
|
|
62
|
+
const puter = getPuter();
|
|
63
|
+
let result;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
result = await puter.hosting.create(subdomain, remoteDir);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error?.error?.code === 'already_in_use') {
|
|
69
|
+
console.log(chalk.yellow(`Subdomain already taken!\nMessage: ${error?.error?.message}`));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.red(`Error when creating "${subdomain}".\nError: ${error?.error?.message}\nCode: ${error?.error?.code}`));
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update a subdomain into remote directory
|
|
79
|
+
* @param {string} subdomain - Subdomain name.
|
|
80
|
+
* @param {string} remoteDir - Remote directory path.
|
|
81
|
+
* @returns {Object} - Hosting details (e.g., subdomain).
|
|
82
|
+
*/
|
|
83
|
+
export async function updateSubdomain(subdomain, remoteDir) {
|
|
84
|
+
const puter = getPuter();
|
|
85
|
+
let result;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
result = await puter.hosting.update(subdomain, remoteDir);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.log(chalk.red(`Error when updating "${subdomain}".\nError: ${error?.message}`));
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|