@portel/photon-core 2.17.6 → 2.18.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.
Files changed (71) hide show
  1. package/dist/audit.d.ts +4 -0
  2. package/dist/audit.d.ts.map +1 -1
  3. package/dist/audit.js +107 -36
  4. package/dist/audit.js.map +1 -1
  5. package/dist/base.d.ts +81 -12
  6. package/dist/base.d.ts.map +1 -1
  7. package/dist/base.js +80 -7
  8. package/dist/base.js.map +1 -1
  9. package/dist/compiler.d.ts.map +1 -1
  10. package/dist/compiler.js +9 -1
  11. package/dist/compiler.js.map +1 -1
  12. package/dist/config.d.ts +14 -28
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +48 -46
  15. package/dist/config.js.map +1 -1
  16. package/dist/data-paths.d.ts +115 -0
  17. package/dist/data-paths.d.ts.map +1 -0
  18. package/dist/data-paths.js +243 -0
  19. package/dist/data-paths.js.map +1 -0
  20. package/dist/dependency-manager.d.ts +1 -1
  21. package/dist/dependency-manager.d.ts.map +1 -1
  22. package/dist/dependency-manager.js +13 -5
  23. package/dist/dependency-manager.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/instance-store.d.ts +22 -11
  29. package/dist/instance-store.d.ts.map +1 -1
  30. package/dist/instance-store.js +63 -28
  31. package/dist/instance-store.js.map +1 -1
  32. package/dist/memory.d.ts +8 -6
  33. package/dist/memory.d.ts.map +1 -1
  34. package/dist/memory.js +49 -35
  35. package/dist/memory.js.map +1 -1
  36. package/dist/mixins.d.ts.map +1 -1
  37. package/dist/mixins.js +22 -3
  38. package/dist/mixins.js.map +1 -1
  39. package/dist/path-resolver.d.ts +3 -1
  40. package/dist/path-resolver.d.ts.map +1 -1
  41. package/dist/path-resolver.js +49 -2
  42. package/dist/path-resolver.js.map +1 -1
  43. package/dist/photon-loader-lite.d.ts +2 -0
  44. package/dist/photon-loader-lite.d.ts.map +1 -1
  45. package/dist/photon-loader-lite.js +3 -3
  46. package/dist/photon-loader-lite.js.map +1 -1
  47. package/dist/schedule.d.ts.map +1 -1
  48. package/dist/schedule.js +11 -7
  49. package/dist/schedule.js.map +1 -1
  50. package/dist/schema-extractor.js +1 -1
  51. package/dist/schema-extractor.js.map +1 -1
  52. package/dist/stateful.d.ts +2 -1
  53. package/dist/stateful.d.ts.map +1 -1
  54. package/dist/stateful.js +4 -3
  55. package/dist/stateful.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/audit.ts +111 -38
  58. package/src/base.ts +117 -19
  59. package/src/compiler.ts +10 -1
  60. package/src/config.ts +59 -46
  61. package/src/data-paths.ts +289 -0
  62. package/src/dependency-manager.ts +13 -5
  63. package/src/index.ts +4 -0
  64. package/src/instance-store.ts +70 -30
  65. package/src/memory.ts +60 -38
  66. package/src/mixins.ts +24 -3
  67. package/src/path-resolver.ts +52 -2
  68. package/src/photon-loader-lite.ts +5 -3
  69. package/src/schedule.ts +11 -7
  70. package/src/schema-extractor.ts +1 -1
  71. package/src/stateful.ts +5 -2
package/src/memory.ts CHANGED
@@ -5,11 +5,11 @@
5
5
  * boilerplate file I/O. Available as `this.memory` on Photon.
6
6
  *
7
7
  * Three scopes:
8
- * | Scope | Meaning | Storage |
9
- * |----------|----------------------------------|-----------------------------------|
10
- * | photon | Private to this photon (default) | ~/.photon/data/{photonId}/ |
11
- * | session | Per-user session (Beam sessions) | ~/.photon/sessions/{sessionId}/ |
12
- * | global | Shared across all photons | ~/.photon/data/_global/ |
8
+ * | Scope | Meaning | Storage |
9
+ * |----------|----------------------------------|-----------------------------------------------|
10
+ * | photon | Private to this photon (default) | .data/{namespace}/{photonName}/memory/ |
11
+ * | session | Per-user session (Beam sessions) | .data/_sessions/{sessionId}/{ns}/{photon}/ |
12
+ * | global | Shared across all photons | .data/_global/ |
13
13
  *
14
14
  * @example
15
15
  * ```typescript
@@ -25,42 +25,60 @@
25
25
  */
26
26
 
27
27
  import * as fs from 'fs/promises';
28
+ import * as fsSync from 'fs';
28
29
  import * as path from 'path';
29
- import * as os from 'os';
30
30
 
31
- export type MemoryScope = 'photon' | 'session' | 'global';
32
-
33
- /**
34
- * Get the base data directory
35
- */
36
- function getDataDir(): string {
37
- return process.env.PHOTON_DATA_DIR || path.join(os.homedir(), '.photon', 'data');
38
- }
31
+ import {
32
+ getPhotonMemoryDir,
33
+ getGlobalMemoryDir,
34
+ getSessionMemoryDir,
35
+ getLegacyMemoryDir,
36
+ getLegacyGlobalMemoryDir,
37
+ getLegacySessionMemoryDir,
38
+ } from './data-paths.js';
39
39
 
40
- /**
41
- * Get the sessions directory
42
- */
43
- function getSessionsDir(): string {
44
- return process.env.PHOTON_SESSIONS_DIR || path.join(os.homedir(), '.photon', 'sessions');
45
- }
40
+ export type MemoryScope = 'photon' | 'session' | 'global';
46
41
 
47
42
  /**
48
- * Resolve storage directory for a given scope
43
+ * Resolve storage directory for a given scope.
44
+ * Uses new .data/ paths with fallback to legacy locations.
49
45
  */
50
- function resolveDir(photonId: string, scope: MemoryScope, sessionId?: string): string {
51
- const safeName = photonId.replace(/[^a-zA-Z0-9_-]/g, '_');
52
-
46
+ function resolveDir(
47
+ photonId: string,
48
+ namespace: string,
49
+ scope: MemoryScope,
50
+ sessionId?: string,
51
+ baseDir?: string
52
+ ): string {
53
53
  switch (scope) {
54
- case 'photon':
55
- return path.join(getDataDir(), safeName);
56
- case 'session':
54
+ case 'photon': {
55
+ const newDir = getPhotonMemoryDir(namespace, photonId, baseDir);
56
+ // Fallback: check legacy path if new path has no data yet
57
+ if (!fsSync.existsSync(newDir)) {
58
+ const legacyDir = getLegacyMemoryDir(photonId, baseDir);
59
+ if (fsSync.existsSync(legacyDir)) return legacyDir;
60
+ }
61
+ return newDir;
62
+ }
63
+ case 'session': {
57
64
  if (!sessionId) {
58
65
  throw new Error('Session ID required for session-scoped memory. Set via memory.sessionId.');
59
66
  }
60
- const safeSession = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
61
- return path.join(getSessionsDir(), safeSession, safeName);
62
- case 'global':
63
- return path.join(getDataDir(), '_global');
67
+ const newDir = getSessionMemoryDir(sessionId, namespace, photonId, baseDir);
68
+ if (!fsSync.existsSync(newDir)) {
69
+ const legacyDir = getLegacySessionMemoryDir(sessionId, photonId, baseDir);
70
+ if (fsSync.existsSync(legacyDir)) return legacyDir;
71
+ }
72
+ return newDir;
73
+ }
74
+ case 'global': {
75
+ const newDir = getGlobalMemoryDir(baseDir);
76
+ if (!fsSync.existsSync(newDir)) {
77
+ const legacyDir = getLegacyGlobalMemoryDir(baseDir);
78
+ if (fsSync.existsSync(legacyDir)) return legacyDir;
79
+ }
80
+ return newDir;
81
+ }
64
82
  default:
65
83
  throw new Error(`Unknown memory scope: ${scope}`);
66
84
  }
@@ -94,11 +112,15 @@ async function pathExists(p: string): Promise<boolean> {
94
112
  */
95
113
  export class MemoryProvider {
96
114
  private _photonId: string;
115
+ private _namespace: string;
97
116
  private _sessionId?: string;
117
+ private _baseDir?: string;
98
118
 
99
- constructor(photonId: string, sessionId?: string) {
119
+ constructor(photonId: string, sessionId?: string, namespace?: string, baseDir?: string) {
100
120
  this._photonId = photonId;
121
+ this._namespace = namespace || 'local';
101
122
  this._sessionId = sessionId;
123
+ this._baseDir = baseDir;
102
124
  }
103
125
 
104
126
  /**
@@ -120,7 +142,7 @@ export class MemoryProvider {
120
142
  * @returns The stored value, or null if not found
121
143
  */
122
144
  async get<T = any>(key: string, scope: MemoryScope = 'photon'): Promise<T | null> {
123
- const dir = resolveDir(this._photonId, scope, this._sessionId);
145
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
124
146
  const filePath = keyPath(dir, key);
125
147
 
126
148
  try {
@@ -140,7 +162,7 @@ export class MemoryProvider {
140
162
  * @param scope Storage scope (default: 'photon')
141
163
  */
142
164
  async set<T = any>(key: string, value: T, scope: MemoryScope = 'photon'): Promise<void> {
143
- const dir = resolveDir(this._photonId, scope, this._sessionId);
165
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
144
166
 
145
167
  if (!await pathExists(dir)) {
146
168
  await fs.mkdir(dir, { recursive: true });
@@ -158,7 +180,7 @@ export class MemoryProvider {
158
180
  * @returns true if the key existed and was deleted
159
181
  */
160
182
  async delete(key: string, scope: MemoryScope = 'photon'): Promise<boolean> {
161
- const dir = resolveDir(this._photonId, scope, this._sessionId);
183
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
162
184
  const filePath = keyPath(dir, key);
163
185
 
164
186
  try {
@@ -177,7 +199,7 @@ export class MemoryProvider {
177
199
  * @param scope Storage scope (default: 'photon')
178
200
  */
179
201
  async has(key: string, scope: MemoryScope = 'photon'): Promise<boolean> {
180
- const dir = resolveDir(this._photonId, scope, this._sessionId);
202
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
181
203
  return pathExists(keyPath(dir, key));
182
204
  }
183
205
 
@@ -187,7 +209,7 @@ export class MemoryProvider {
187
209
  * @param scope Storage scope (default: 'photon')
188
210
  */
189
211
  async keys(scope: MemoryScope = 'photon'): Promise<string[]> {
190
- const dir = resolveDir(this._photonId, scope, this._sessionId);
212
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
191
213
 
192
214
  try {
193
215
  const files = await fs.readdir(dir);
@@ -206,7 +228,7 @@ export class MemoryProvider {
206
228
  * @param scope Storage scope (default: 'photon')
207
229
  */
208
230
  async clear(scope: MemoryScope = 'photon'): Promise<void> {
209
- const dir = resolveDir(this._photonId, scope, this._sessionId);
231
+ const dir = resolveDir(this._photonId, this._namespace, scope, this._sessionId, this._baseDir);
210
232
 
211
233
  try {
212
234
  const files = await fs.readdir(dir);
package/src/mixins.ts CHANGED
@@ -57,6 +57,7 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
57
57
  * @internal
58
58
  */
59
59
  _photonName?: string;
60
+ _photonNamespace?: string;
60
61
 
61
62
  /**
62
63
  * Session ID for session-scoped memory - set by runtime
@@ -112,7 +113,7 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
112
113
  .replace(/([A-Z])/g, '-$1')
113
114
  .toLowerCase()
114
115
  .replace(/^-/, '');
115
- this._memory = new MemoryProvider(name, this._sessionId);
116
+ this._memory = new MemoryProvider(name, this._sessionId, this._photonNamespace);
116
117
  }
117
118
  return this._memory;
118
119
  }
@@ -134,14 +135,34 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
134
135
 
135
136
  /**
136
137
  * Render a formatted value as an intermediate result.
138
+ * Supports UI feedback formats (status, progress, toast) and data formats (table, qr, etc.).
137
139
  * Each call replaces the previous render. Call with no args to clear.
138
140
  */
139
141
  protected render(format?: string, value?: any): void {
140
142
  if (format === undefined) {
141
143
  this.emit({ emit: 'render:clear' });
142
- } else {
143
- this.emit({ emit: 'render', format, value });
144
+ return;
144
145
  }
146
+ switch (format) {
147
+ case 'status':
148
+ this.emit(typeof value === 'string' ? { emit: 'status', message: value } : { emit: 'status', ...value });
149
+ return;
150
+ case 'progress':
151
+ this.emit(typeof value === 'number' ? { emit: 'progress', value } : { emit: 'progress', ...value });
152
+ return;
153
+ case 'toast':
154
+ this.emit(typeof value === 'string' ? { emit: 'toast', message: value } : { emit: 'toast', ...value });
155
+ return;
156
+ }
157
+ this.emit({ emit: 'render', format, value });
158
+ }
159
+
160
+ /**
161
+ * Create a blocking input request for use in generator methods.
162
+ * Returns a yield object: `const name = yield this.ask('text', 'Name?');`
163
+ */
164
+ protected ask(type: string, message: string, options?: Record<string, any>): { ask: string; message: string; [key: string]: any } {
165
+ return { ask: type, message, ...options };
145
166
  }
146
167
 
147
168
  /**
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import * as fs from 'fs/promises';
17
+ import * as fsSync from 'fs';
17
18
  import * as path from 'path';
18
19
  import * as os from 'os';
19
20
 
@@ -47,7 +48,8 @@ const defaultOptions: Required<ResolverOptions> = {
47
48
 
48
49
  /** Directories to skip when scanning for namespace subdirectories */
49
50
  const SKIP_DIRS = new Set([
50
- 'state', 'context', 'env', '.cache', '.config',
51
+ '.data', '.cache', '.config',
52
+ 'state', 'context', 'env', // legacy data dirs (pre-.data/ consolidation)
51
53
  'node_modules', 'marketplace', 'photons', 'templates',
52
54
  ]);
53
55
 
@@ -243,12 +245,60 @@ export async function listFilesWithNamespace(
243
245
  return results;
244
246
  }
245
247
 
248
+ /** Runtime data patterns that should never be committed to a marketplace repo */
249
+ const GITIGNORE_DATA_PATTERNS = [
250
+ '# Photon runtime data (auto-generated)',
251
+ '.data/',
252
+ ];
253
+
254
+ /**
255
+ * Check if a directory is inside a git repository
256
+ */
257
+ function isGitRepo(dir: string): boolean {
258
+ let current = dir;
259
+ while (true) {
260
+ if (fsSync.existsSync(path.join(current, '.git'))) return true;
261
+ const parent = path.dirname(current);
262
+ if (parent === current) return false;
263
+ current = parent;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Ensure .gitignore in a git-tracked photon dir excludes runtime data.
269
+ * Only adds missing patterns — never removes or overwrites existing entries.
270
+ */
271
+ async function ensureGitignore(dir: string): Promise<void> {
272
+ const gitignorePath = path.join(dir, '.gitignore');
273
+ let existing = '';
274
+ try {
275
+ existing = await fs.readFile(gitignorePath, 'utf-8');
276
+ } catch {
277
+ // No .gitignore yet
278
+ }
279
+
280
+ const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
281
+ const missing = GITIGNORE_DATA_PATTERNS.filter((p) => !existingLines.has(p));
282
+
283
+ if (missing.length === 0) return;
284
+
285
+ const append = (existing && !existing.endsWith('\n') ? '\n' : '') + missing.join('\n') + '\n';
286
+ await fs.appendFile(gitignorePath, append);
287
+ }
288
+
246
289
  /**
247
- * Ensure directory exists
290
+ * Ensure directory exists.
291
+ * If the directory is inside a git repo (e.g., a marketplace repo),
292
+ * auto-generates .gitignore entries for runtime data directories.
248
293
  */
249
294
  export async function ensureDir(dir?: string): Promise<void> {
250
295
  const targetDir = expandTilde(dir || DEFAULT_PHOTON_DIR);
251
296
  await fs.mkdir(targetDir, { recursive: true });
297
+
298
+ // Auto-exclude runtime data when developing in a git-tracked marketplace repo
299
+ if (targetDir !== DEFAULT_PHOTON_DIR && isGitRepo(targetDir)) {
300
+ await ensureGitignore(targetDir);
301
+ }
252
302
  }
253
303
 
254
304
  // Convenience aliases for photon-specific usage
@@ -38,6 +38,7 @@ import { ScheduleProvider } from './schedule.js';
38
38
  import { toEnvVarName, parseEnvValue, type MissingParamInfo } from './env-utils.js';
39
39
  import type { ExtractedSchema } from './types.js';
40
40
  import type { MCPClientFactory } from '@portel/mcp';
41
+ import { getCacheDir } from './data-paths.js';
41
42
 
42
43
  // ═══════════════════════════════════════════════════════════════════
43
44
  // Types
@@ -54,6 +55,8 @@ export interface PhotonOptions {
54
55
  onEvent?: (event: PhotonEvent) => void;
55
56
  /** Session ID for session-scoped memory */
56
57
  sessionId?: string;
58
+ /** Namespace for data path resolution (marketplace owner). Defaults to 'local'. */
59
+ namespace?: string;
57
60
  }
58
61
 
59
62
  export interface PhotonEvent {
@@ -154,9 +157,7 @@ async function loadPhotonInternal(
154
157
  const source = await fs.readFile(absolutePath, 'utf-8');
155
158
 
156
159
  // 2. Compile TypeScript → JavaScript
157
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
158
- const cacheDir = path.join(homeDir, '.photon', 'cache');
159
- const compiledPath = await compilePhotonTS(absolutePath, { cacheDir });
160
+ const compiledPath = await compilePhotonTS(absolutePath, { cacheDir: getCacheDir() });
160
161
 
161
162
  // 3. Import compiled module
162
163
  const moduleUrl = pathToFileURL(compiledPath).href;
@@ -193,6 +194,7 @@ async function loadPhotonInternal(
193
194
 
194
195
  // 10. Set photon identity
195
196
  instance._photonName = photonName;
197
+ instance._photonNamespace = options.namespace || 'local';
196
198
  if (options.instanceName) {
197
199
  instance.instanceName = options.instanceName;
198
200
  }
package/src/schedule.ts CHANGED
@@ -31,6 +31,9 @@
31
31
  import * as fs from 'fs/promises';
32
32
  import * as path from 'path';
33
33
  import * as os from 'os';
34
+ import * as fsSync from 'fs';
35
+
36
+ import { getPhotonSchedulesDir, getLegacySchedulesDir } from './data-paths.js';
34
37
  import { randomUUID } from 'crypto';
35
38
 
36
39
  // ── Types ──────────────────────────────────────────────────────────────
@@ -136,13 +139,14 @@ function resolveCron(schedule: string): string {
136
139
 
137
140
  // ── Storage Helpers ────────────────────────────────────────────────────
138
141
 
139
- function getSchedulesDir(): string {
140
- return process.env.PHOTON_SCHEDULES_DIR || path.join(os.homedir(), '.photon', 'schedules');
141
- }
142
-
143
- function photonScheduleDir(photonId: string): string {
144
- const safeName = photonId.replace(/[^a-zA-Z0-9_-]/g, '_');
145
- return path.join(getSchedulesDir(), safeName);
142
+ function photonScheduleDir(photonId: string, namespace?: string): string {
143
+ const ns = namespace || 'local';
144
+ const newDir = getPhotonSchedulesDir(ns, photonId);
145
+ if (!fsSync.existsSync(newDir)) {
146
+ const legacyDir = getLegacySchedulesDir(photonId);
147
+ if (fsSync.existsSync(legacyDir)) return legacyDir;
148
+ }
149
+ return newDir;
146
150
  }
147
151
 
148
152
  function taskPath(photonId: string, taskId: string): string {
@@ -2365,7 +2365,7 @@ export class SchemaExtractor {
2365
2365
  if (['metric', 'gauge', 'progress', 'badge', 'timeline', 'dashboard', 'cart', 'qr', 'slides',
2366
2366
  'steps', 'stepper', 'log', 'image', 'hero', 'banner', 'quote', 'profile', 'heatmap',
2367
2367
  'kanban', 'calendar', 'map', 'cron', 'comparison', 'invoice', 'receipt', 'network', 'graph',
2368
- 'checklist', 'article', 'magazine'].includes(format)) {
2368
+ 'checklist', 'article', 'magazine', 'guide'].includes(format)) {
2369
2369
  return format as OutputFormat;
2370
2370
  }
2371
2371
 
package/src/stateful.ts CHANGED
@@ -54,6 +54,8 @@
54
54
  import * as fs from 'fs/promises';
55
55
  import * as path from 'path';
56
56
  import * as os from 'os';
57
+
58
+ import { getPhotonRunsDir, getLegacyRunsDir } from './data-paths.js';
57
59
  import { createReadStream } from 'fs';
58
60
  import { createInterface } from 'readline';
59
61
  import { executionContext } from '@portel/cli';
@@ -85,9 +87,10 @@ import {
85
87
  // ══════════════════════════════════════════════════════════════════════════════
86
88
 
87
89
  /**
88
- * Default runs directory (~/.photon/runs)
90
+ * Default runs directory (legacy: ~/.photon/runs)
91
+ * @deprecated Use getPhotonRunsDir(namespace, photonName) from data-paths.ts
89
92
  */
90
- export const RUNS_DIR = path.join(os.homedir(), '.photon', 'runs');
93
+ export const RUNS_DIR = getLegacyRunsDir();
91
94
 
92
95
  // ══════════════════════════════════════════════════════════════════════════════
93
96
  // CHECKPOINT YIELD TYPE