skedyul 1.0.2 → 1.0.4

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.
@@ -33,6 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.generateProfileName = generateProfileName;
37
+ exports.ensureUniqueProfileName = ensureUniqueProfileName;
38
+ exports.getProfiles = getProfiles;
39
+ exports.saveProfiles = saveProfiles;
40
+ exports.getProfile = getProfile;
41
+ exports.saveProfile = saveProfile;
42
+ exports.deleteProfile = deleteProfile;
43
+ exports.listProfiles = listProfiles;
44
+ exports.clearAllProfiles = clearAllProfiles;
45
+ exports.getActiveProfileName = getActiveProfileName;
46
+ exports.setActiveProfile = setActiveProfile;
36
47
  exports.getCredentials = getCredentials;
37
48
  exports.saveCredentials = saveCredentials;
38
49
  exports.clearCredentials = clearCredentials;
@@ -52,9 +63,9 @@ const http = __importStar(require("http"));
52
63
  // Paths
53
64
  // ─────────────────────────────────────────────────────────────────────────────
54
65
  const SKEDYUL_HOME_DIR = path.join(os.homedir(), '.skedyul');
55
- const CREDENTIALS_FILE = path.join(SKEDYUL_HOME_DIR, 'credentials.json');
66
+ const PROFILES_FILE = path.join(SKEDYUL_HOME_DIR, 'profiles.json');
56
67
  const CONFIG_FILE = path.join(SKEDYUL_HOME_DIR, 'config.json');
57
- // Local project config (for development overrides)
68
+ const LEGACY_CREDENTIALS_FILE = path.join(SKEDYUL_HOME_DIR, 'credentials.json');
58
69
  const LOCAL_CONFIG_FILE = '.skedyul.local.json';
59
70
  const DEFAULT_SERVER_URL = 'https://admin.skedyul.it';
60
71
  // ─────────────────────────────────────────────────────────────────────────────
@@ -66,37 +77,234 @@ function ensureHomeDir() {
66
77
  }
67
78
  }
68
79
  // ─────────────────────────────────────────────────────────────────────────────
69
- // Credentials Management
80
+ // Profile Name Generation
70
81
  // ─────────────────────────────────────────────────────────────────────────────
71
- function getCredentials() {
72
- if (!fs.existsSync(CREDENTIALS_FILE)) {
73
- return null;
74
- }
82
+ /**
83
+ * Generate a sensible profile name from a server URL.
84
+ */
85
+ function generateProfileName(serverUrl) {
75
86
  try {
76
- const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
77
- const credentials = JSON.parse(content);
78
- // Check if expired
79
- if (credentials.expiresAt) {
80
- const expiresAt = new Date(credentials.expiresAt);
81
- if (expiresAt < new Date()) {
82
- return null;
83
- }
87
+ const url = new URL(serverUrl);
88
+ const hostname = url.hostname.toLowerCase();
89
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
90
+ return 'local';
91
+ }
92
+ if (hostname.includes('staging')) {
93
+ return 'staging';
94
+ }
95
+ if (hostname === 'admin.skedyul.it' || hostname === 'app.skedyul.com') {
96
+ return 'production';
84
97
  }
85
- return credentials;
98
+ const parts = hostname.split('.');
99
+ if (parts.length > 0) {
100
+ return parts[0].replace(/[^a-z0-9-]/g, '');
101
+ }
102
+ return 'default';
86
103
  }
87
104
  catch {
88
- return null;
105
+ return 'default';
106
+ }
107
+ }
108
+ /**
109
+ * Ensure a profile name is unique by appending a number if needed.
110
+ */
111
+ function ensureUniqueProfileName(baseName, existingProfiles) {
112
+ if (!existingProfiles[baseName]) {
113
+ return baseName;
114
+ }
115
+ let counter = 2;
116
+ while (existingProfiles[`${baseName}-${counter}`]) {
117
+ counter++;
118
+ }
119
+ return `${baseName}-${counter}`;
120
+ }
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Migration from Legacy credentials.json
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+ function migrateLegacyCredentials() {
125
+ if (!fs.existsSync(LEGACY_CREDENTIALS_FILE)) {
126
+ return;
127
+ }
128
+ if (fs.existsSync(PROFILES_FILE)) {
129
+ return;
130
+ }
131
+ try {
132
+ const content = fs.readFileSync(LEGACY_CREDENTIALS_FILE, 'utf-8');
133
+ const legacy = JSON.parse(content);
134
+ const profileName = generateProfileName(legacy.serverUrl);
135
+ const profile = {
136
+ serverUrl: legacy.serverUrl,
137
+ token: legacy.token,
138
+ userId: legacy.userId,
139
+ username: legacy.username,
140
+ email: legacy.email,
141
+ expiresAt: legacy.expiresAt,
142
+ createdAt: legacy.createdAt,
143
+ };
144
+ const profilesFile = {
145
+ profiles: {
146
+ [profileName]: profile,
147
+ },
148
+ };
149
+ ensureHomeDir();
150
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(profilesFile, null, 2), {
151
+ mode: 0o600,
152
+ });
153
+ const config = getConfig();
154
+ config.activeProfile = profileName;
155
+ saveConfig(config);
156
+ fs.unlinkSync(LEGACY_CREDENTIALS_FILE);
157
+ console.error(`Migrated credentials to profile: ${profileName}`);
158
+ }
159
+ catch (error) {
160
+ console.error('Failed to migrate legacy credentials:', error);
89
161
  }
90
162
  }
91
- function saveCredentials(credentials) {
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+ // Profiles Management
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+ function getProfiles() {
167
+ migrateLegacyCredentials();
168
+ if (!fs.existsSync(PROFILES_FILE)) {
169
+ return { profiles: {} };
170
+ }
171
+ try {
172
+ const content = fs.readFileSync(PROFILES_FILE, 'utf-8');
173
+ return JSON.parse(content);
174
+ }
175
+ catch {
176
+ return { profiles: {} };
177
+ }
178
+ }
179
+ function saveProfiles(profilesFile) {
92
180
  ensureHomeDir();
93
- fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
94
- mode: 0o600, // Read/write for owner only
181
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(profilesFile, null, 2), {
182
+ mode: 0o600,
95
183
  });
96
184
  }
185
+ function getProfile(name) {
186
+ const profilesFile = getProfiles();
187
+ const profile = profilesFile.profiles[name];
188
+ if (!profile) {
189
+ return null;
190
+ }
191
+ if (profile.expiresAt) {
192
+ const expiresAt = new Date(profile.expiresAt);
193
+ if (expiresAt < new Date()) {
194
+ return null;
195
+ }
196
+ }
197
+ return profile;
198
+ }
199
+ function saveProfile(name, profile) {
200
+ const profilesFile = getProfiles();
201
+ profilesFile.profiles[name] = profile;
202
+ saveProfiles(profilesFile);
203
+ }
204
+ function deleteProfile(name) {
205
+ const profilesFile = getProfiles();
206
+ if (!profilesFile.profiles[name]) {
207
+ return false;
208
+ }
209
+ delete profilesFile.profiles[name];
210
+ saveProfiles(profilesFile);
211
+ const config = getConfig();
212
+ if (config.activeProfile === name) {
213
+ const remainingProfiles = Object.keys(profilesFile.profiles);
214
+ config.activeProfile = remainingProfiles[0] ?? undefined;
215
+ saveConfig(config);
216
+ }
217
+ return true;
218
+ }
219
+ function listProfiles() {
220
+ const profilesFile = getProfiles();
221
+ const config = getConfig();
222
+ const activeProfile = config.activeProfile;
223
+ return Object.entries(profilesFile.profiles).map(([name, profile]) => {
224
+ let isExpired = false;
225
+ if (profile.expiresAt) {
226
+ isExpired = new Date(profile.expiresAt) < new Date();
227
+ }
228
+ return {
229
+ name,
230
+ profile,
231
+ isActive: name === activeProfile,
232
+ isExpired,
233
+ };
234
+ });
235
+ }
236
+ function clearAllProfiles() {
237
+ if (fs.existsSync(PROFILES_FILE)) {
238
+ fs.unlinkSync(PROFILES_FILE);
239
+ }
240
+ const config = getConfig();
241
+ delete config.activeProfile;
242
+ saveConfig(config);
243
+ }
244
+ // ─────────────────────────────────────────────────────────────────────────────
245
+ // Active Profile Management
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ function getActiveProfileName() {
248
+ const config = getConfig();
249
+ return config.activeProfile ?? null;
250
+ }
251
+ function setActiveProfile(name) {
252
+ const profilesFile = getProfiles();
253
+ if (!profilesFile.profiles[name]) {
254
+ return false;
255
+ }
256
+ const config = getConfig();
257
+ config.activeProfile = name;
258
+ saveConfig(config);
259
+ return true;
260
+ }
261
+ // ─────────────────────────────────────────────────────────────────────────────
262
+ // Credentials Management (uses active profile)
263
+ // ─────────────────────────────────────────────────────────────────────────────
264
+ function getCredentials() {
265
+ const activeProfileName = getActiveProfileName();
266
+ if (!activeProfileName) {
267
+ return null;
268
+ }
269
+ const profile = getProfile(activeProfileName);
270
+ if (!profile) {
271
+ return null;
272
+ }
273
+ return {
274
+ token: profile.token,
275
+ userId: profile.userId,
276
+ username: profile.username,
277
+ email: profile.email,
278
+ serverUrl: profile.serverUrl,
279
+ expiresAt: profile.expiresAt,
280
+ createdAt: profile.createdAt,
281
+ };
282
+ }
283
+ function saveCredentials(credentials, profileName) {
284
+ const name = profileName ?? generateProfileName(credentials.serverUrl);
285
+ const profilesFile = getProfiles();
286
+ const finalName = profileName
287
+ ? name
288
+ : ensureUniqueProfileName(name, profilesFile.profiles);
289
+ const profile = {
290
+ serverUrl: credentials.serverUrl,
291
+ token: credentials.token,
292
+ userId: credentials.userId,
293
+ username: credentials.username,
294
+ email: credentials.email,
295
+ expiresAt: credentials.expiresAt,
296
+ createdAt: credentials.createdAt,
297
+ };
298
+ saveProfile(finalName, profile);
299
+ const config = getConfig();
300
+ config.activeProfile = finalName;
301
+ saveConfig(config);
302
+ return finalName;
303
+ }
97
304
  function clearCredentials() {
98
- if (fs.existsSync(CREDENTIALS_FILE)) {
99
- fs.unlinkSync(CREDENTIALS_FILE);
305
+ const activeProfileName = getActiveProfileName();
306
+ if (activeProfileName) {
307
+ deleteProfile(activeProfileName);
100
308
  }
101
309
  }
102
310
  // ─────────────────────────────────────────────────────────────────────────────
@@ -137,21 +345,17 @@ function getLocalConfig() {
137
345
  }
138
346
  /**
139
347
  * Get the server URL to use.
140
- * Priority: CLI flag > local config > credentials > global config > default
348
+ * Priority: CLI flag > local config > active profile > global config > default
141
349
  */
142
350
  function getServerUrl(override) {
143
- // 1. CLI flag takes precedence
144
351
  if (override)
145
352
  return override;
146
- // 2. Local project config (for development)
147
353
  const localConfig = getLocalConfig();
148
354
  if (localConfig.serverUrl)
149
355
  return localConfig.serverUrl;
150
- // 3. Stored credentials
151
356
  const credentials = getCredentials();
152
357
  if (credentials?.serverUrl)
153
358
  return credentials.serverUrl;
154
- // 4. Global config
155
359
  return getConfig().defaultServer;
156
360
  }
157
361
  /**
@@ -183,7 +387,6 @@ async function startOAuthCallback(serverUrl) {
183
387
  const server = http.createServer((req, res) => {
184
388
  const url = new URL(req.url ?? '/', `http://localhost`);
185
389
  const searchParams = url.searchParams;
186
- // Handle callback
187
390
  if (url.pathname === '/callback') {
188
391
  const token = searchParams.get('token');
189
392
  const userId = searchParams.get('userId');
@@ -227,7 +430,7 @@ async function startOAuthCallback(serverUrl) {
227
430
  res.end(`
228
431
  <html>
229
432
  <body style="font-family: system-ui; padding: 40px; text-align: center;">
230
- <h1 style="color: #38a169;">✓ Authentication Successful</h1>
433
+ <h1 style="color: #38a169;">Authentication Successful</h1>
231
434
  <p>Logged in as <strong>${email}</strong></p>
232
435
  <p>You can close this window and return to the terminal.</p>
233
436
  </body>
@@ -248,7 +451,6 @@ async function startOAuthCallback(serverUrl) {
248
451
  res.end('Not found');
249
452
  }
250
453
  });
251
- // Find an available port
252
454
  server.listen(0, '127.0.0.1', () => {
253
455
  const address = server.address();
254
456
  if (!address || typeof address === 'string') {
@@ -262,7 +464,6 @@ async function startOAuthCallback(serverUrl) {
262
464
  console.log(`Opening browser for authentication...`);
263
465
  console.log(`If browser doesn't open, visit: ${authUrl}`);
264
466
  console.log(`Waiting for callback on ${callbackUrl}...`);
265
- // Try to open browser
266
467
  openBrowser(authUrl).catch(() => {
267
468
  console.log(`(Could not open browser automatically)`);
268
469
  });
@@ -271,7 +472,6 @@ async function startOAuthCallback(serverUrl) {
271
472
  cleanup();
272
473
  reject(err);
273
474
  });
274
- // Timeout after 5 minutes
275
475
  timeoutId = setTimeout(() => {
276
476
  server.close();
277
477
  reject(new Error('Authentication timed out'));
@@ -282,13 +482,11 @@ async function startOAuthCallback(serverUrl) {
282
482
  * Open a URL in the default browser
283
483
  */
284
484
  async function openBrowser(url) {
285
- // Try to dynamically import 'open' package
286
485
  try {
287
486
  const open = await Promise.resolve().then(() => __importStar(require('open')));
288
487
  await open.default(url);
289
488
  }
290
489
  catch {
291
- // Fallback to platform-specific commands
292
490
  const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
293
491
  const platform = process.platform;
294
492
  let command;
@@ -23,10 +23,6 @@ import type { ResourceDependency } from './resource';
23
23
  * - 'object': JSON object
24
24
  */
25
25
  export type FieldType = 'string' | 'long_string' | 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'file' | 'image' | 'relation' | 'object';
26
- /**
27
- * Relationship cardinality between models.
28
- */
29
- export type Cardinality = 'one_to_one' | 'one_to_many' | 'many_to_one' | 'many_to_many';
30
26
  /**
31
27
  * Behavior when a related record is deleted.
32
28
  * - 'none': No action (orphan the reference)
@@ -122,17 +118,26 @@ export interface RelationshipLink {
122
118
  field: string;
123
119
  /** Display label for this side of the relationship */
124
120
  label: string;
125
- /** Cardinality from this side's perspective */
126
- cardinality: Cardinality;
127
- /** Behavior when this record is deleted */
128
- onDelete?: OnDelete;
129
121
  }
122
+ /**
123
+ * Relationship cardinality from source (one) to target (many or one).
124
+ * - 'one_to_one': One source record relates to one target record
125
+ * - 'one_to_many': One source record relates to many target records
126
+ *
127
+ * Note: For many-to-one relationships, swap source and target and use 'one_to_many'.
128
+ */
129
+ export type Cardinality = 'one_to_one' | 'one_to_many';
130
130
  /**
131
131
  * Relationship definition between two models.
132
+ * Source is always the "one" side, target is the "many" side (for one_to_many).
132
133
  */
133
134
  export interface RelationshipDefinition {
134
- /** Source side of the relationship */
135
+ /** Source side of the relationship (the "one" side) */
135
136
  source: RelationshipLink;
136
- /** Target side of the relationship */
137
+ /** Target side of the relationship (the "many" side for one_to_many) */
137
138
  target: RelationshipLink;
139
+ /** Cardinality: 'one_to_one' or 'one_to_many' */
140
+ cardinality: Cardinality;
141
+ /** Behavior when a related record is deleted */
142
+ onDelete?: OnDelete;
138
143
  }