twentythree-cli 1.0.1 → 1.1.1

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.
@@ -0,0 +1,87 @@
1
+ require("../../_virtual/_rolldown/runtime.cjs");
2
+ const require_lib_term_map = require("../../lib/term-map.cjs");
3
+ const require_lib_base_command = require("../../lib/base-command.cjs");
4
+ const require_lib_output = require("../../lib/output.cjs");
5
+ let _oclif_core = require("@oclif/core");
6
+ //#region src/commands/app/list.ts
7
+ /**
8
+ * App list command — returns paginated list of player design apps on the workspace.
9
+ */
10
+ var AppList = class AppList extends require_lib_base_command.AuthenticatedCommand {
11
+ static description = "List player design apps on the active workspace";
12
+ static examples = [
13
+ "<%= config.bin %> app list",
14
+ "<%= config.bin %> app list --app-id 42",
15
+ "<%= config.bin %> app list --page 2 --size 50",
16
+ "<%= config.bin %> app list --json"
17
+ ];
18
+ static enableJsonFlag = true;
19
+ static agentMetadata = {
20
+ api_endpoint: "POST /app/list",
21
+ auth_scope: "read",
22
+ output_shape: {
23
+ type: "table",
24
+ columns: [
25
+ "ID",
26
+ "Name",
27
+ "Type",
28
+ "Description"
29
+ ]
30
+ },
31
+ side_effects: "none"
32
+ };
33
+ static flags = {
34
+ ...require_lib_base_command.AuthenticatedCommand.baseFlags,
35
+ "app-id": _oclif_core.Flags.integer({
36
+ description: "Filter results to a specific app ID",
37
+ required: false
38
+ }),
39
+ page: _oclif_core.Flags.integer({
40
+ description: "Page offset",
41
+ required: false
42
+ }),
43
+ size: _oclif_core.Flags.integer({
44
+ description: "Number of results per page (default 20, max 100)",
45
+ required: false
46
+ })
47
+ };
48
+ static args = {};
49
+ async run() {
50
+ const { flags } = await this.parse(AppList);
51
+ this.printWorkspaceHeader();
52
+ const body = {};
53
+ if (flags["app-id"] !== void 0) body.app_id = flags["app-id"];
54
+ if (flags.page !== void 0) body.p = flags.page;
55
+ if (flags.size !== void 0) body.size = flags.size;
56
+ const { data, error } = await this.apiClient.POST("/app/list", {
57
+ body,
58
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
59
+ });
60
+ if (error) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(error)), { exit: 1 });
61
+ const apps = data?.apps ?? data?.data ?? [];
62
+ if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
63
+ ok: true,
64
+ data: apps,
65
+ summary: "App list",
66
+ breadcrumbs: [{ domain: this.activeWorkspace.domain }, { resource: "app" }]
67
+ });
68
+ if (apps.length === 0) {
69
+ this.log("No apps found.");
70
+ return;
71
+ }
72
+ const table = require_lib_output.renderTable([
73
+ "ID",
74
+ "Name",
75
+ "Type",
76
+ "Description"
77
+ ], apps.map((a) => [
78
+ String(a.app_id ?? a.id ?? ""),
79
+ String(a.name ?? ""),
80
+ String(a.type ?? ""),
81
+ String(a.description ?? "")
82
+ ]));
83
+ this.log(table.toString());
84
+ }
85
+ };
86
+ //#endregion
87
+ module.exports = AppList;
@@ -0,0 +1,51 @@
1
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
2
+ const require_lib_term_map = require("../../lib/term-map.cjs");
3
+ const require_lib_base_command = require("../../lib/base-command.cjs");
4
+ const require_lib_output = require("../../lib/output.cjs");
5
+ let _oclif_core = require("@oclif/core");
6
+ let chalk = require("chalk");
7
+ chalk = require_runtime.__toESM(chalk);
8
+ //#region src/commands/app/remove-thumbnail.ts
9
+ /**
10
+ * App remove-thumbnail command — removes the custom thumbnail for an app, reverting to default.
11
+ *
12
+ * Threat mitigations:
13
+ * T-08-05: extends AuthenticatedCommand — anonymous mode rejected
14
+ */
15
+ var AppRemoveThumbnail = class AppRemoveThumbnail extends require_lib_base_command.AuthenticatedCommand {
16
+ static description = "Remove the custom thumbnail for an app, reverting to the default";
17
+ static examples = ["<%= config.bin %> app remove-thumbnail 42", "<%= config.bin %> app remove-thumbnail 42 --json"];
18
+ static enableJsonFlag = true;
19
+ static agentMetadata = {
20
+ api_endpoint: "POST /app/remove-thumbnail",
21
+ auth_scope: "write",
22
+ output_shape: { type: "key-value" },
23
+ side_effects: "updates"
24
+ };
25
+ static flags = { ...require_lib_base_command.AuthenticatedCommand.baseFlags };
26
+ static args = { id: _oclif_core.Args.string({
27
+ description: "App ID",
28
+ required: true
29
+ }) };
30
+ async run() {
31
+ const { args } = await this.parse(AppRemoveThumbnail);
32
+ this.printWorkspaceHeader();
33
+ const { data, error } = await this.apiClient.POST("/app/remove-thumbnail", {
34
+ body: { app_id: Number(args.id) },
35
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
36
+ });
37
+ if (error) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(error)), { exit: 1 });
38
+ this.log(chalk.default.green(`Thumbnail removed for app ${args.id}`));
39
+ if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
40
+ ok: true,
41
+ data,
42
+ summary: `Thumbnail removed for app ${args.id}`,
43
+ breadcrumbs: [{ domain: this.activeWorkspace.domain }, {
44
+ resource: "app",
45
+ id: args.id
46
+ }]
47
+ });
48
+ }
49
+ };
50
+ //#endregion
51
+ module.exports = AppRemoveThumbnail;
@@ -0,0 +1,78 @@
1
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
2
+ const require_lib_term_map = require("../../lib/term-map.cjs");
3
+ const require_lib_base_command = require("../../lib/base-command.cjs");
4
+ const require_lib_output = require("../../lib/output.cjs");
5
+ let _oclif_core = require("@oclif/core");
6
+ let chalk = require("chalk");
7
+ chalk = require_runtime.__toESM(chalk);
8
+ let node_fs = require("node:fs");
9
+ node_fs = require_runtime.__toESM(node_fs);
10
+ let node_path = require("node:path");
11
+ node_path = require_runtime.__toESM(node_path);
12
+ //#region src/commands/app/set-thumbnail.ts
13
+ /**
14
+ * App set-thumbnail command — uploads and sets a custom thumbnail image for an app.
15
+ *
16
+ * Uses direct multipart POST with bodySerializer to create FormData.
17
+ * Per D-3: NOT the chunked engine — thumbnail images use direct multipart upload.
18
+ *
19
+ * Threat mitigations:
20
+ * T-08-08: Validates file exists via fs.existsSync before reading
21
+ * T-08-05: extends AuthenticatedCommand — anonymous mode rejected
22
+ */
23
+ var AppSetThumbnail = class AppSetThumbnail extends require_lib_base_command.AuthenticatedCommand {
24
+ static description = "Upload and set a custom thumbnail image for an app";
25
+ static examples = ["<%= config.bin %> app set-thumbnail ./thumbnail.png --app-id 42", "<%= config.bin %> app set-thumbnail ./thumbnail.jpg --app-id 42 --json"];
26
+ static enableJsonFlag = true;
27
+ static agentMetadata = {
28
+ api_endpoint: "POST /app/set-thumbnail",
29
+ auth_scope: "write",
30
+ output_shape: { type: "key-value" },
31
+ side_effects: "updates"
32
+ };
33
+ static flags = {
34
+ ...require_lib_base_command.AuthenticatedCommand.baseFlags,
35
+ "app-id": _oclif_core.Flags.integer({
36
+ description: "App ID to update",
37
+ required: true
38
+ })
39
+ };
40
+ static args = { file: _oclif_core.Args.string({
41
+ description: "Path to the thumbnail image file",
42
+ required: true
43
+ }) };
44
+ async run() {
45
+ const { args, flags } = await this.parse(AppSetThumbnail);
46
+ this.printWorkspaceHeader();
47
+ const filePath = node_path.resolve(args.file);
48
+ if (!node_fs.existsSync(filePath)) this.error(`File not found: ${filePath}`, { exit: 1 });
49
+ const fileBuffer = node_fs.readFileSync(filePath);
50
+ const fileName = node_path.basename(filePath);
51
+ const fileBlob = new Blob([fileBuffer]);
52
+ const { data, error } = await this.apiClient.POST("/app/set-thumbnail", {
53
+ body: {
54
+ app_id: flags["app-id"],
55
+ file: fileBlob
56
+ },
57
+ bodySerializer(body) {
58
+ const fd = new FormData();
59
+ for (const [k, v] of Object.entries(body)) if (v !== void 0) if (v instanceof Blob) fd.append(k, v, fileName);
60
+ else fd.append(k, String(v));
61
+ return fd;
62
+ }
63
+ });
64
+ if (error) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(error)), { exit: 1 });
65
+ this.log(chalk.default.green(`Thumbnail set for app ${flags["app-id"]}`));
66
+ if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
67
+ ok: true,
68
+ data,
69
+ summary: `Thumbnail set for app ${flags["app-id"]}`,
70
+ breadcrumbs: [{ domain: this.activeWorkspace.domain }, {
71
+ resource: "app",
72
+ id: String(flags["app-id"])
73
+ }]
74
+ });
75
+ }
76
+ };
77
+ //#endregion
78
+ module.exports = AppSetThumbnail;
@@ -40,13 +40,14 @@ var Credentials = class Credentials extends _oclif_core.Command {
40
40
  require_auth_workspace_config.setCredentialDomain(domain);
41
41
  const s = _clack_prompts.spinner();
42
42
  s.start("Discovering workspaces...");
43
- let workspaces;
43
+ let workspaces = [];
44
44
  try {
45
45
  workspaces = await require_auth_token_refresh.fetchWorkspaceTokens(domain, trimmedToken);
46
46
  s.stop("Workspaces discovered");
47
47
  } catch (err) {
48
48
  s.stop("Failed to discover workspaces");
49
49
  this.error(`Could not discover workspaces: ${err instanceof Error ? err.message : String(err)}`, { exit: 1 });
50
+ return;
50
51
  }
51
52
  let selectedWorkspaces;
52
53
  if (workspaces.length > 1) {
@@ -0,0 +1,79 @@
1
+ const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
2
+ const require_lib_detect_shell = require("../../lib/detect-shell.cjs");
3
+ let _oclif_core = require("@oclif/core");
4
+ let _clack_prompts = require("@clack/prompts");
5
+ _clack_prompts = require_runtime.__toESM(_clack_prompts);
6
+ //#region src/commands/autocomplete/index.ts
7
+ var Autocomplete = class Autocomplete extends _oclif_core.Command {
8
+ static description = "Set up tab completion for your shell";
9
+ static agentMetadata = {
10
+ api_endpoint: "interactive",
11
+ auth_scope: "none",
12
+ output_shape: { type: "none" },
13
+ side_effects: "creates"
14
+ };
15
+ static examples = ["<%= config.bin %> autocomplete"];
16
+ async run() {
17
+ await this.parse(Autocomplete);
18
+ _clack_prompts.intro("Tab completion setup");
19
+ const detectedShell = require_lib_detect_shell.detectShell(process.env.SHELL ?? "");
20
+ let shell;
21
+ if (detectedShell) {
22
+ const confirm = await _clack_prompts.confirm({ message: `Detected shell: ${detectedShell}. Set up completion for ${detectedShell}?` });
23
+ if (_clack_prompts.isCancel(confirm)) {
24
+ _clack_prompts.cancel("Cancelled");
25
+ return;
26
+ }
27
+ if (confirm) shell = detectedShell;
28
+ else {
29
+ const chosen = await _clack_prompts.select({
30
+ message: "Select your shell",
31
+ options: [{
32
+ value: "zsh",
33
+ label: "zsh"
34
+ }, {
35
+ value: "bash",
36
+ label: "bash"
37
+ }]
38
+ });
39
+ if (_clack_prompts.isCancel(chosen)) {
40
+ _clack_prompts.cancel("Cancelled");
41
+ return;
42
+ }
43
+ shell = chosen;
44
+ }
45
+ } else {
46
+ const chosen = await _clack_prompts.select({
47
+ message: "Select your shell",
48
+ options: [{
49
+ value: "zsh",
50
+ label: "zsh"
51
+ }, {
52
+ value: "bash",
53
+ label: "bash"
54
+ }]
55
+ });
56
+ if (_clack_prompts.isCancel(chosen)) {
57
+ _clack_prompts.cancel("Cancelled");
58
+ return;
59
+ }
60
+ shell = chosen;
61
+ }
62
+ const s = _clack_prompts.spinner();
63
+ s.start("Building completion cache...");
64
+ try {
65
+ await this.config.runCommand("autocomplete:create", []);
66
+ s.stop("Completion cache built");
67
+ } catch (err) {
68
+ s.stop("Failed to build completion cache");
69
+ this.error(`Could not build completion cache: ${err instanceof Error ? err.message : String(err)}`, { exit: 1 });
70
+ return;
71
+ }
72
+ const rcFile = shell === "zsh" ? "~/.zshrc" : "~/.bashrc";
73
+ const evalLine = `grep -qF 'twentythree autocomplete script ${shell}' ${rcFile} || printf "$(twentythree autocomplete script ${shell})" >> ${rcFile}; source ${rcFile}`;
74
+ _clack_prompts.note(`Add tab completion to your shell by running:\n\n ${evalLine}\n\nThen restart your terminal or run: source ${rcFile}`, "Setup instructions");
75
+ _clack_prompts.outro("After setup, try: twentythree video <TAB>");
76
+ }
77
+ };
78
+ //#endregion
79
+ module.exports = Autocomplete;
@@ -52,7 +52,7 @@ var VideoList = class VideoList extends require_lib_base_command.AuthenticatedCo
52
52
  async run() {
53
53
  const { flags } = await this.parse(VideoList);
54
54
  this.printWorkspaceHeader();
55
- const videos = await require_lib_pagination.fetchAllPages(async (page, size) => {
55
+ const allVideos = await require_lib_pagination.fetchAllPages(async (page, size) => {
56
56
  const { data, error } = await this.apiClient.GET("/photo/list", { params: { query: {
57
57
  p: page,
58
58
  size,
@@ -65,6 +65,7 @@ var VideoList = class VideoList extends require_lib_base_command.AuthenticatedCo
65
65
  total_count: resp?.total_count
66
66
  };
67
67
  });
68
+ const videos = flags.limit !== void 0 ? allVideos.slice(0, flags.limit) : allVideos;
68
69
  if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
69
70
  ok: true,
70
71
  data: videos,
@@ -81,7 +81,7 @@ var VideoReplace = class VideoReplace extends require_lib_base_command.Authentic
81
81
  this.error(`File not found: ${args.file}`, { exit: 1 });
82
82
  }
83
83
  const { data: tokenData, error: tokenError } = await this.apiClient.GET("/photo/get-replace-token", { params: { query: { photo_id: Number(args.id) } } });
84
- if (tokenError) this.error(require_lib_term_map.applyCliTerms(String(tokenError)), { exit: 1 });
84
+ if (tokenError) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(tokenError)), { exit: 1 });
85
85
  const replaceToken = tokenData?.data?.replace_token;
86
86
  if (!replaceToken) this.error("Failed to obtain replace token from API", { exit: 1 });
87
87
  const totalBytes = (await (0, node_fs_promises.stat)(args.file)).size;
@@ -107,6 +107,7 @@ var VideoReplace = class VideoReplace extends require_lib_base_command.Authentic
107
107
  } finally {
108
108
  bar.finish();
109
109
  }
110
+ if (!result) return;
110
111
  const adminUrl = `https://${this.activeWorkspace.domain}/manage/video/${args.id}`;
111
112
  this.log(chalk.default.green(`Video ${args.id} replaced successfully`));
112
113
  this.log(`ID: ${args.id}`);
@@ -0,0 +1,54 @@
1
+ const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
2
+ const require_lib_term_map = require("../../../lib/term-map.cjs");
3
+ const require_lib_base_command = require("../../../lib/base-command.cjs");
4
+ const require_lib_output = require("../../../lib/output.cjs");
5
+ let _oclif_core = require("@oclif/core");
6
+ let chalk = require("chalk");
7
+ chalk = require_runtime.__toESM(chalk);
8
+ //#region src/commands/video/section/generate.ts
9
+ /**
10
+ * Video section generate command — AI-generates sections from a video transcript.
11
+ *
12
+ * Replaces all existing sections. Requires a transcript to be available.
13
+ */
14
+ var VideoSectionGenerate = class VideoSectionGenerate extends require_lib_base_command.AuthenticatedCommand {
15
+ static description = "Automatically generate sections for a video using AI (requires transcript)";
16
+ static examples = ["<%= config.bin %> video section generate 12345", "<%= config.bin %> video section generate 12345 --json"];
17
+ static enableJsonFlag = true;
18
+ static agentMetadata = {
19
+ api_endpoint: "POST /photo/section/generate",
20
+ auth_scope: "write",
21
+ output_shape: { type: "key-value" },
22
+ side_effects: "creates"
23
+ };
24
+ static flags = { ...require_lib_base_command.AuthenticatedCommand.baseFlags };
25
+ static args = { id: _oclif_core.Args.string({
26
+ description: "Video ID",
27
+ required: true
28
+ }) };
29
+ async run() {
30
+ const { args } = await this.parse(VideoSectionGenerate);
31
+ this.printWorkspaceHeader();
32
+ const { data, error } = await this.apiClient.POST("/photo/section/generate", {
33
+ body: { photo_id: Number(args.id) },
34
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
35
+ });
36
+ if (error) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(error)), { exit: 1 });
37
+ this.log(chalk.default.green(`Sections generated for video ${args.id}`));
38
+ if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
39
+ ok: true,
40
+ data,
41
+ summary: `Sections generated for video ${args.id}`,
42
+ breadcrumbs: [
43
+ { domain: this.activeWorkspace.domain },
44
+ {
45
+ resource: "video",
46
+ id: args.id
47
+ },
48
+ { resource: "section" }
49
+ ]
50
+ });
51
+ }
52
+ };
53
+ //#endregion
54
+ module.exports = VideoSectionGenerate;
@@ -146,9 +146,12 @@ var VideoUpdate = class VideoUpdate extends require_lib_base_command.Authenticat
146
146
  initialValue: current.published_p ? "yes" : "no"
147
147
  });
148
148
  if ((0, _clack_prompts.isCancel)(publishedResult)) process.exit(2);
149
- body.title = titleResult;
150
- body.description = descriptionResult;
151
- body.tags = tagsResult;
149
+ const titleVal = titleResult;
150
+ const descriptionVal = descriptionResult;
151
+ const tagsVal = tagsResult;
152
+ if (titleVal !== "") body.title = titleVal;
153
+ if (descriptionVal !== "" || current.content_text === "") body.description = descriptionVal;
154
+ if (tagsVal !== "" || currentTags === "") body.tags = tagsVal;
152
155
  body.published_p = publishedResult === "yes" ? 1 : 0;
153
156
  } else {
154
157
  if (flags.title !== void 0) body.title = flags.title;
@@ -167,7 +170,7 @@ var VideoUpdate = class VideoUpdate extends require_lib_base_command.Authenticat
167
170
  body,
168
171
  headers: { "Content-Type": "application/x-www-form-urlencoded" }
169
172
  });
170
- if (updateError) this.error(require_lib_term_map.applyCliTerms(String(updateError)), { exit: 1 });
173
+ if (updateError) this.error(require_lib_term_map.applyCliTerms(require_lib_output.formatApiError(updateError)), { exit: 1 });
171
174
  this.log(chalk.default.green(`Video ${args.id} updated`));
172
175
  if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
173
176
  ok: true,
@@ -49,7 +49,10 @@ var WebinarMailPreview = class WebinarMailPreview extends require_lib_base_comma
49
49
  const { args, flags } = await this.parse(WebinarMailPreview);
50
50
  this.printWorkspaceHeader();
51
51
  const contextField = flags["webinar-id"] ? { live_id: Number(flags["webinar-id"]) } : flags["series-id"] ? { live_series_id: Number(flags["series-id"]) } : null;
52
- if (!contextField) this.error(require_lib_term_map.applyCliTerms("Either --webinar-id or --series-id is required"), { exit: 1 });
52
+ if (!contextField) {
53
+ this.error(require_lib_term_map.applyCliTerms("Either --webinar-id or --series-id is required"), { exit: 1 });
54
+ return;
55
+ }
53
56
  const query = new URLSearchParams({
54
57
  ...Object.fromEntries(Object.entries(contextField).map(([k, v]) => [k, String(v)])),
55
58
  live_mail_id: String(args.id)
@@ -57,8 +60,18 @@ var WebinarMailPreview = class WebinarMailPreview extends require_lib_base_comma
57
60
  const headers = {};
58
61
  if (this.activeWorkspace.bearer_token) headers["Authorization"] = `Bearer ${this.activeWorkspace.bearer_token}`;
59
62
  const response = await fetch(`${this.apiBaseUrl}live/mail/preview?${query}`, { headers });
60
- const html = await response.text();
61
- if (!response.ok) this.error(require_lib_term_map.applyCliTerms(`API error ${response.status}: ${html}`), { exit: 1 });
63
+ const body = await response.text();
64
+ if (!response.ok) {
65
+ let errMsg;
66
+ try {
67
+ const parsed = JSON.parse(body);
68
+ errMsg = parsed?.message ?? parsed?.error ?? `status ${response.status}`;
69
+ } catch {
70
+ errMsg = body.slice(0, 200) || `status ${response.status}`;
71
+ }
72
+ this.error(require_lib_term_map.applyCliTerms(`API error: ${errMsg}`), { exit: 1 });
73
+ }
74
+ const html = body;
62
75
  if (this.jsonEnabled()) return require_lib_output.formatJsonOutput({
63
76
  ok: true,
64
77
  data: html,
@@ -7,6 +7,7 @@ let _oclif_core = require("@oclif/core");
7
7
  let chalk = require("chalk");
8
8
  chalk = require_runtime.__toESM(chalk);
9
9
  let _clack_prompts = require("@clack/prompts");
10
+ _clack_prompts = require_runtime.__toESM(_clack_prompts);
10
11
  //#region src/lib/base-command.ts
11
12
  var BaseCommand = class extends _oclif_core.Command {
12
13
  static enableJsonFlag = true;
@@ -71,7 +72,7 @@ var BaseCommand = class extends _oclif_core.Command {
71
72
  const result = require_auth_workspace_config.findWorkspace(workspaceFlagValue, require_auth_workspace_config.getWorkspaces());
72
73
  if (result === null) this.error(`No workspace matching '${workspaceFlagValue}' found — run \`twentythree workspace list\` to see available workspaces`, { exit: 1 });
73
74
  else if (Array.isArray(result)) {
74
- const chosen = await (0, _clack_prompts.select)({
75
+ const chosen = await _clack_prompts.select({
75
76
  message: `Multiple workspaces match '${workspaceFlagValue}'. Select one:`,
76
77
  options: result.map((w) => ({
77
78
  value: w.domain,
@@ -79,7 +80,9 @@ var BaseCommand = class extends _oclif_core.Command {
79
80
  }))
80
81
  });
81
82
  if (typeof chosen === "symbol") this.error("Workspace selection cancelled", { exit: 1 });
82
- resolved = require_auth_workspace_config.getWorkspaceForDomain(chosen);
83
+ const found = require_auth_workspace_config.getWorkspaceForDomain(chosen);
84
+ if (!found) this.error(`Workspace '${chosen}' could not be resolved — try running \`twentythree workspace list\``, { exit: 1 });
85
+ resolved = found;
83
86
  } else resolved = result;
84
87
  } else {
85
88
  const activeDomain = require_auth_workspace_config.getActiveWorkspace();
@@ -100,6 +103,31 @@ var BaseCommand = class extends _oclif_core.Command {
100
103
  token: this.activeWorkspace.bearer_token || void 0
101
104
  });
102
105
  }
106
+ async catch(err) {
107
+ if (!process.stdin.isTTY) return super.catch(err);
108
+ if (err.constructor.name !== "FailedFlagValidationError") return super.catch(err);
109
+ const flagNames = [...err.message.matchAll(/Missing required flag ([^\n]+)/g)].map((m) => m[1]);
110
+ if (flagNames.length === 0) return super.catch(err);
111
+ const inputFlags = err.parse?.input?.flags ?? {};
112
+ _clack_prompts.intro("Missing required input");
113
+ const extraArgv = [];
114
+ for (const flagName of flagNames) {
115
+ const flagDef = inputFlags[flagName];
116
+ const label = flagDef?.description ?? flagDef?.summary ?? flagName;
117
+ const value = await _clack_prompts.text({
118
+ message: label,
119
+ validate: (v) => !v || v.trim().length === 0 ? "Value is required" : void 0
120
+ });
121
+ if (_clack_prompts.isCancel(value)) {
122
+ _clack_prompts.cancel("Cancelled");
123
+ throw new _oclif_core.Errors.CLIError("Cancelled", { exit: 0 });
124
+ }
125
+ extraArgv.push(`--${flagName}`, value);
126
+ }
127
+ _clack_prompts.outro("Running command...");
128
+ const newArgv = [...this.argv ?? [], ...extraArgv];
129
+ await this.config.runCommand(this.id, newArgv);
130
+ }
103
131
  /**
104
132
  * Print the [domain] workspace header in dim style.
105
133
  * Call at the top of every command's run() method (AUTH-04).
@@ -0,0 +1,13 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/lib/detect-shell.ts
3
+ /**
4
+ * Detect shell from the $SHELL environment variable.
5
+ * Returns 'zsh', 'bash', or null for unrecognized / unset shells.
6
+ */
7
+ function detectShell(shellEnv) {
8
+ if (shellEnv.endsWith("zsh")) return "zsh";
9
+ if (shellEnv.endsWith("bash")) return "bash";
10
+ return null;
11
+ }
12
+ //#endregion
13
+ exports.detectShell = detectShell;
@@ -35,7 +35,21 @@ twentythree workspace list
35
35
  twentythree workspace use company.video23.com
36
36
  ```
37
37
 
38
- ## Step 3: Run your first command
38
+ ## Step 3: Enable tab completion (optional)
39
+
40
+ Run this once to set up `<TAB>` completion for all commands and flags:
41
+
42
+ ```bash
43
+ twentythree autocomplete
44
+ ```
45
+
46
+ The command detects your shell (bash or zsh), builds the completion cache, and shows the eval line to paste into your RC file. After sourcing your RC file or restarting your terminal, try:
47
+
48
+ ```bash
49
+ twentythree video <TAB>
50
+ ```
51
+
52
+ ## Step 4: Run your first command
39
53
 
40
54
  List the videos in your active workspace.
41
55
 
@@ -64,3 +78,5 @@ Run `twentythree <topic> --help` for help on any topic. For example:
64
78
  twentythree video --help
65
79
  twentythree auth --help
66
80
  ```
81
+
82
+ Use `<TAB>` after any command to discover subcommands and flags.