superlab 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -68,6 +68,28 @@ superlab init --all
68
68
 
69
69
  `--all` is an alias for `--platform both`. Use `--force` if you need to overwrite existing files.
70
70
 
71
+ ## Update
72
+
73
+ Refresh the current project:
74
+
75
+ ```bash
76
+ superlab update
77
+ ```
78
+
79
+ Refresh a specific project:
80
+
81
+ ```bash
82
+ superlab update --target /path/to/project
83
+ ```
84
+
85
+ Refresh every registered project initialized from this user account:
86
+
87
+ ```bash
88
+ superlab update --all-projects
89
+ ```
90
+
91
+ `superlab init` writes `.superlab/install.json` inside each project and registers the project in the user-level registry so `update --all-projects` knows what to refresh.
92
+
71
93
  ## Language
72
94
 
73
95
  The installer chooses the display language from your system locale. If it detects Chinese locale values, it installs Chinese-facing command and skill text; otherwise it falls back to English.
package/README.zh-CN.md CHANGED
@@ -66,6 +66,28 @@ superlab init --all
66
66
 
67
67
  `--all` 是 `--platform both` 的别名。如果目标目录已有同名文件,可以加 `--force` 覆盖。
68
68
 
69
+ ## 更新
70
+
71
+ 刷新当前项目:
72
+
73
+ ```bash
74
+ superlab update
75
+ ```
76
+
77
+ 刷新指定项目:
78
+
79
+ ```bash
80
+ superlab update --target /path/to/project
81
+ ```
82
+
83
+ 刷新当前用户登记过的所有项目:
84
+
85
+ ```bash
86
+ superlab update --all-projects
87
+ ```
88
+
89
+ `superlab init` 会在项目内写入 `.superlab/install.json`,并在用户级 registry 里登记项目路径,所以 `update --all-projects` 才知道要刷新哪些项目。
90
+
69
91
  ## 语言
70
92
 
71
93
  安装器会根据系统 locale 自动猜测展示语言。检测到中文 locale 时会安装中文命令和技能文案;否则默认安装英文文案。
package/bin/superlab.cjs CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const path = require("node:path");
4
- const { detectLanguage, installSuperlab } = require("../lib/install.cjs");
4
+ const {
5
+ PACKAGE_VERSION,
6
+ detectLanguage,
7
+ getProjectVersionInfo,
8
+ installSuperlab,
9
+ updateAllProjects,
10
+ updateSuperlabProject,
11
+ } = require("../lib/install.cjs");
5
12
  const { promptSelect } = require("../lib/init_tui.cjs");
6
13
 
7
14
  function printHelp() {
@@ -10,10 +17,15 @@ function printHelp() {
10
17
  Usage:
11
18
  superlab init [--target <dir>] [--platform codex|claude|both|all] [--lang en|zh] [--force]
12
19
  superlab install [--target <dir>] [--platform codex|claude|both|all] [--lang en|zh] [--force]
20
+ superlab update [--target <dir>]
21
+ superlab update --all-projects
22
+ superlab version [--target <dir>] [--global|--project]
13
23
 
14
24
  Commands:
15
25
  init Initialize /lab commands, skills, templates, and scripts in a target
16
26
  install Backward-compatible alias for init
27
+ update Refresh an initialized project or all registered projects
28
+ version Show installed CLI version and project asset version
17
29
  help Show this help message
18
30
  `);
19
31
  }
@@ -56,6 +68,81 @@ function parseInstallArgs(argv) {
56
68
  return options;
57
69
  }
58
70
 
71
+ function parseUpdateArgs(argv) {
72
+ const options = {
73
+ targetDir: process.cwd(),
74
+ allProjects: false,
75
+ };
76
+
77
+ for (let index = 0; index < argv.length; index += 1) {
78
+ const value = argv[index];
79
+ if (value === "--target") {
80
+ options.targetDir = path.resolve(argv[index + 1]);
81
+ index += 1;
82
+ } else if (value === "--all-projects") {
83
+ options.allProjects = true;
84
+ } else {
85
+ throw new Error(`Unknown option: ${value}`);
86
+ }
87
+ }
88
+
89
+ if (options.allProjects && argv.includes("--target")) {
90
+ throw new Error("Use either --target or --all-projects, not both.");
91
+ }
92
+
93
+ return options;
94
+ }
95
+
96
+ function parseVersionArgs(argv) {
97
+ const options = {
98
+ targetDir: process.cwd(),
99
+ globalOnly: false,
100
+ projectOnly: false,
101
+ };
102
+
103
+ for (let index = 0; index < argv.length; index += 1) {
104
+ const value = argv[index];
105
+ if (value === "--target") {
106
+ options.targetDir = path.resolve(argv[index + 1]);
107
+ index += 1;
108
+ } else if (value === "--global") {
109
+ options.globalOnly = true;
110
+ } else if (value === "--project") {
111
+ options.projectOnly = true;
112
+ } else {
113
+ throw new Error(`Unknown option: ${value}`);
114
+ }
115
+ }
116
+
117
+ if (options.globalOnly && options.projectOnly) {
118
+ throw new Error("Use either --global or --project, not both.");
119
+ }
120
+
121
+ return options;
122
+ }
123
+
124
+ function printVersion(options) {
125
+ const lines = [];
126
+ if (!options.projectOnly) {
127
+ lines.push(`cli: ${PACKAGE_VERSION}`);
128
+ }
129
+ if (!options.globalOnly) {
130
+ const projectInfo = getProjectVersionInfo({ targetDir: options.targetDir });
131
+ if (projectInfo.status === "managed") {
132
+ lines.push(`project: ${projectInfo.package_version}`);
133
+ lines.push(`platform: ${projectInfo.platform}`);
134
+ lines.push(`language: ${projectInfo.lang}`);
135
+ } else if (projectInfo.status === "legacy") {
136
+ lines.push("project: legacy");
137
+ lines.push(`platform: ${projectInfo.platform}`);
138
+ lines.push(`language: ${projectInfo.lang}`);
139
+ } else {
140
+ lines.push("project: not initialized");
141
+ }
142
+ }
143
+ console.log(lines.join("\n"));
144
+ }
145
+
59
146
  function shouldUseInteractiveInit(options) {
60
147
  if (options.lang && options.platform) {
61
148
  return false;
@@ -107,16 +194,45 @@ async function main() {
107
194
  return;
108
195
  }
109
196
 
110
- if (!["init", "install"].includes(command)) {
197
+ if (!["init", "install", "update", "version"].includes(command)) {
111
198
  throw new Error(`Unknown command: ${command}`);
112
199
  }
113
200
 
201
+ if (command === "version") {
202
+ printVersion(parseVersionArgs(rest));
203
+ return;
204
+ }
205
+
206
+ if (command === "update") {
207
+ const options = parseUpdateArgs(rest);
208
+ if (options.allProjects) {
209
+ const result = updateAllProjects();
210
+ console.log(`updated projects: ${result.updated.length}`);
211
+ for (const project of result.updated) {
212
+ console.log(`updated: ${project}`);
213
+ }
214
+ for (const project of result.skipped) {
215
+ console.log(`skipped: ${project.path} (${project.reason})`);
216
+ }
217
+ return;
218
+ }
219
+
220
+ const metadata = updateSuperlabProject({ targetDir: options.targetDir });
221
+ console.log(`superlab updated in ${options.targetDir}`);
222
+ console.log(`platform: ${metadata.platform}`);
223
+ console.log(`language: ${metadata.lang}`);
224
+ if (metadata.migration) {
225
+ console.log(`migration: ${metadata.migration}`);
226
+ }
227
+ return;
228
+ }
229
+
114
230
  const parsedOptions = parseInstallArgs(rest);
115
231
  const options = await resolveInitOptions(parsedOptions);
116
232
  if (options.platform === "all") {
117
233
  options.platform = "both";
118
234
  }
119
- installSuperlab(options);
235
+ installSuperlab({ ...options, registerProject: true });
120
236
  console.log(`superlab installed into ${options.targetDir}`);
121
237
  console.log(`platform: ${options.platform}`);
122
238
  console.log(`language: ${options.lang}`);
package/lib/install.cjs CHANGED
@@ -1,8 +1,10 @@
1
1
  const fs = require("node:fs");
2
+ const os = require("node:os");
2
3
  const path = require("node:path");
3
4
  const { getLocalizedContent } = require("./i18n.cjs");
4
5
 
5
6
  const REPO_ROOT = path.resolve(__dirname, "..");
7
+ const PACKAGE_VERSION = require(path.join(REPO_ROOT, "package.json")).version;
6
8
 
7
9
  const ASSET_GROUPS = {
8
10
  codex: [
@@ -102,6 +104,152 @@ function chmodScripts(targetDir) {
102
104
  }
103
105
  }
104
106
 
107
+ function superlabHomeDir({ env = process.env } = {}) {
108
+ if (env.SUPERLAB_HOME) {
109
+ return env.SUPERLAB_HOME;
110
+ }
111
+ return path.join(env.HOME || os.homedir(), ".superlab");
112
+ }
113
+
114
+ function registryFilePath({ env = process.env } = {}) {
115
+ return path.join(superlabHomeDir({ env }), "registry.json");
116
+ }
117
+
118
+ function installMetadataPath(targetDir) {
119
+ return path.join(targetDir, ".superlab", "install.json");
120
+ }
121
+
122
+ function ensureParentDir(filePath) {
123
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
124
+ }
125
+
126
+ function writeProjectInstallMetadata(targetDir, metadata) {
127
+ const filePath = installMetadataPath(targetDir);
128
+ ensureParentDir(filePath);
129
+ fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2) + "\n");
130
+ }
131
+
132
+ function readProjectInstallMetadata(targetDir) {
133
+ const filePath = installMetadataPath(targetDir);
134
+ if (!fs.existsSync(filePath)) {
135
+ throw new Error(`No superlab installation metadata found in ${targetDir}. Run 'superlab init' first.`);
136
+ }
137
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
138
+ }
139
+
140
+ function detectLegacyPlatform(targetDir) {
141
+ const hasCodex =
142
+ fs.existsSync(path.join(targetDir, ".codex", "prompts")) ||
143
+ fs.existsSync(path.join(targetDir, ".codex", "skills", "lab"));
144
+ const hasClaude =
145
+ fs.existsSync(path.join(targetDir, ".claude", "commands", "lab")) ||
146
+ fs.existsSync(path.join(targetDir, ".claude", "skills", "lab"));
147
+
148
+ if (hasCodex && hasClaude) {
149
+ return "both";
150
+ }
151
+ if (hasCodex) {
152
+ return "codex";
153
+ }
154
+ if (hasClaude) {
155
+ return "claude";
156
+ }
157
+ return null;
158
+ }
159
+
160
+ function looksChinese(text) {
161
+ return /[\u3400-\u9fff]/.test(text);
162
+ }
163
+
164
+ function detectLegacyLanguage(targetDir) {
165
+ const workflowConfigPath = path.join(targetDir, ".superlab", "config", "workflow.json");
166
+ if (fs.existsSync(workflowConfigPath)) {
167
+ try {
168
+ const config = JSON.parse(fs.readFileSync(workflowConfigPath, "utf8"));
169
+ if (config.workflow_language === "zh" || config.workflow_language === "en") {
170
+ return config.workflow_language;
171
+ }
172
+ } catch {
173
+ // Fall through to file-content heuristics.
174
+ }
175
+ }
176
+
177
+ const probeFiles = [
178
+ path.join(targetDir, ".codex", "prompts", "lab-idea.md"),
179
+ path.join(targetDir, ".claude", "commands", "lab", "idea.md"),
180
+ path.join(targetDir, ".superlab", "templates", "idea.md"),
181
+ ];
182
+
183
+ for (const probeFile of probeFiles) {
184
+ if (!fs.existsSync(probeFile)) {
185
+ continue;
186
+ }
187
+ const content = fs.readFileSync(probeFile, "utf8");
188
+ return looksChinese(content) ? "zh" : "en";
189
+ }
190
+
191
+ return "en";
192
+ }
193
+
194
+ function detectLegacyInstallMetadata(targetDir) {
195
+ const platform = detectLegacyPlatform(targetDir);
196
+ if (!platform) {
197
+ return null;
198
+ }
199
+
200
+ return {
201
+ target_dir: path.resolve(targetDir),
202
+ platform,
203
+ lang: detectLegacyLanguage(targetDir),
204
+ package_version: "legacy",
205
+ updated_at: null,
206
+ legacy: true,
207
+ };
208
+ }
209
+
210
+ function readRegistry({ env = process.env } = {}) {
211
+ const filePath = registryFilePath({ env });
212
+ if (!fs.existsSync(filePath)) {
213
+ return { projects: [] };
214
+ }
215
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
216
+ }
217
+
218
+ function writeRegistry(registry, { env = process.env } = {}) {
219
+ const filePath = registryFilePath({ env });
220
+ ensureParentDir(filePath);
221
+ fs.writeFileSync(filePath, JSON.stringify(registry, null, 2) + "\n");
222
+ }
223
+
224
+ function registerProjectInstall(targetDir, metadata, { env = process.env } = {}) {
225
+ const registry = readRegistry({ env });
226
+ const normalizedTarget = path.resolve(targetDir);
227
+ const entry = {
228
+ path: normalizedTarget,
229
+ platform: metadata.platform,
230
+ lang: metadata.lang,
231
+ package_version: metadata.package_version,
232
+ updated_at: metadata.updated_at,
233
+ };
234
+ const existingIndex = registry.projects.findIndex((project) => project.path === normalizedTarget);
235
+ if (existingIndex >= 0) {
236
+ registry.projects[existingIndex] = entry;
237
+ } else {
238
+ registry.projects.push(entry);
239
+ }
240
+ writeRegistry(registry, { env });
241
+ }
242
+
243
+ function isTemporaryTestPath(targetDir) {
244
+ const resolvedTarget = path.resolve(targetDir);
245
+ const tmpRoot = path.resolve(os.tmpdir());
246
+ const relativeToTmp = path.relative(tmpRoot, resolvedTarget);
247
+ if (relativeToTmp.startsWith("..") || path.isAbsolute(relativeToTmp)) {
248
+ return false;
249
+ }
250
+ return path.basename(resolvedTarget).startsWith("superlab-");
251
+ }
252
+
105
253
  function detectLanguage({ explicitLang, env = process.env } = {}) {
106
254
  if (explicitLang) {
107
255
  if (!["en", "zh"].includes(explicitLang)) {
@@ -175,7 +323,14 @@ function localizeInstalledAssets(targetDir, lang) {
175
323
  }
176
324
  }
177
325
 
178
- function installSuperlab({ targetDir, platform = "both", force = false, lang } = {}) {
326
+ function installSuperlab({
327
+ targetDir,
328
+ platform = "both",
329
+ force = false,
330
+ lang,
331
+ env = process.env,
332
+ registerProject = false,
333
+ } = {}) {
179
334
  const groups = [];
180
335
  if (platform === "codex" || platform === "both") {
181
336
  groups.push(...ASSET_GROUPS.codex);
@@ -188,11 +343,126 @@ function installSuperlab({ targetDir, platform = "both", force = false, lang } =
188
343
  for (const asset of groups) {
189
344
  copyDirectory(asset.from, path.join(targetDir, asset.to), force);
190
345
  }
191
- localizeInstalledAssets(targetDir, detectLanguage({ explicitLang: lang }));
346
+ const resolvedLang = detectLanguage({ explicitLang: lang, env });
347
+ localizeInstalledAssets(targetDir, resolvedLang);
192
348
  chmodScripts(targetDir);
349
+ const metadata = {
350
+ target_dir: path.resolve(targetDir),
351
+ platform,
352
+ lang: resolvedLang,
353
+ package_version: PACKAGE_VERSION,
354
+ updated_at: new Date().toISOString(),
355
+ };
356
+ writeProjectInstallMetadata(targetDir, metadata);
357
+ if (registerProject) {
358
+ registerProjectInstall(targetDir, metadata, { env });
359
+ }
360
+ return metadata;
361
+ }
362
+
363
+ function updateSuperlabProject({ targetDir, env = process.env } = {}) {
364
+ let metadata;
365
+ let migratedLegacy = false;
366
+ try {
367
+ metadata = readProjectInstallMetadata(targetDir);
368
+ } catch (error) {
369
+ metadata = detectLegacyInstallMetadata(targetDir);
370
+ if (!metadata) {
371
+ throw error;
372
+ }
373
+ migratedLegacy = true;
374
+ }
375
+
376
+ const installedMetadata = installSuperlab({
377
+ targetDir,
378
+ platform: metadata.platform,
379
+ lang: metadata.lang,
380
+ force: true,
381
+ env,
382
+ registerProject: true,
383
+ });
384
+ if (migratedLegacy) {
385
+ installedMetadata.migration = "legacy project metadata reconstructed";
386
+ }
387
+ return installedMetadata;
388
+ }
389
+
390
+ function updateAllProjects({ env = process.env } = {}) {
391
+ const registry = readRegistry({ env });
392
+ const updated = [];
393
+ const skipped = [];
394
+ const retained = [];
395
+
396
+ for (const project of registry.projects) {
397
+ try {
398
+ if (isTemporaryTestPath(project.path)) {
399
+ skipped.push({ path: project.path, reason: "temporary path pruned from registry" });
400
+ continue;
401
+ }
402
+ if (!fs.existsSync(project.path)) {
403
+ skipped.push({ path: project.path, reason: "missing project path" });
404
+ continue;
405
+ }
406
+ const metadata = updateSuperlabProject({ targetDir: project.path, env });
407
+ updated.push(project.path);
408
+ retained.push({
409
+ path: path.resolve(project.path),
410
+ platform: metadata.platform,
411
+ lang: metadata.lang,
412
+ package_version: metadata.package_version,
413
+ updated_at: metadata.updated_at,
414
+ });
415
+ } catch (error) {
416
+ skipped.push({ path: project.path, reason: error.message });
417
+ if (fs.existsSync(project.path)) {
418
+ retained.push(project);
419
+ }
420
+ }
421
+ }
422
+
423
+ writeRegistry({ projects: retained }, { env });
424
+ return { updated, skipped };
425
+ }
426
+
427
+ function getProjectVersionInfo({ targetDir } = {}) {
428
+ try {
429
+ const metadata = readProjectInstallMetadata(targetDir);
430
+ return {
431
+ status: "managed",
432
+ package_version: metadata.package_version,
433
+ platform: metadata.platform,
434
+ lang: metadata.lang,
435
+ };
436
+ } catch {
437
+ const legacyMetadata = detectLegacyInstallMetadata(targetDir);
438
+ if (!legacyMetadata) {
439
+ return {
440
+ status: "missing",
441
+ package_version: null,
442
+ platform: null,
443
+ lang: null,
444
+ };
445
+ }
446
+ return {
447
+ status: "legacy",
448
+ package_version: "legacy",
449
+ platform: legacyMetadata.platform,
450
+ lang: legacyMetadata.lang,
451
+ };
452
+ }
193
453
  }
194
454
 
195
455
  module.exports = {
456
+ PACKAGE_VERSION,
196
457
  detectLanguage,
458
+ detectLegacyInstallMetadata,
197
459
  installSuperlab,
460
+ installMetadataPath,
461
+ isTemporaryTestPath,
462
+ getProjectVersionInfo,
463
+ readProjectInstallMetadata,
464
+ registryFilePath,
465
+ readRegistry,
466
+ updateAllProjects,
467
+ updateSuperlabProject,
198
468
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlab",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Strict /lab research workflow installer for Codex and Claude",
5
5
  "keywords": [
6
6
  "codex",